Funktionale C#-Programmierung im Detail (2) Benannte Funktion und Funktionspolymorphismus

Funktionale C#-Programmierung im Detail (2) Benannte Funktion und Funktionspolymorphismus

[LINQ via C#-Reihe]

[Eingehende Serie zur funktionalen Programmierung in C#]

Neueste Version:https://weblogs.asp.net/dixin/functional-csharp-named-function-and-static-instance-extension-method

In C# sind die intuitivsten Funktionen Methodenmember von Klassen und Strukturen, einschließlich statischer Methoden, Instanzmethoden und Erweiterungsmethoden usw. Diese Methoden haben beim Design Namen und werden nach Namen aufgerufen, also sind sie benannte Funktionen. Einige andere methodenähnliche Member, darunter statischer Konstruktor, Konstruktor, Finalizer, Konvertierungsoperator, Operatorüberladung, Eigenschaft, Indexer, Ereigniszugriff, sind ebenfalls benannte Funktionen, deren spezifischer Name zur Kompilierungszeit generiert wird. In diesem Kapitel werden benannte Funktionen in C# behandelt, wie diese benannten Funktionen definiert werden und wie sie funktionieren. Der Name des Methodenmembers ist zur Entwurfszeit verfügbar, während der Name einiger anderer Funktionsmember zur Kompilierzeit generiert wird.

Konstruktor, statischer Konstruktor und Finalizer

Klasse und Struktur können einen Konstruktor, einen statischen Konstruktor und einen Finalizer haben. Der Konstruktor kann auf statische und Instanzmember zugreifen und wird normalerweise zum Initialisieren von Instanzmembern verwendet. Der statische Konstruktor kann nur auf statische Member zugreifen und wird zur Laufzeit nur einmal automatisch aufgerufen, bevor die erste Instanz erstellt wird oder bevor auf einen statischen Member zugegriffen wird. Die Klasse kann auch über einen Finalizer verfügen, der normalerweise nicht verwaltete Ressourcen bereinigt, der automatisch aufgerufen wird, bevor die Instanz zur Laufzeit von der Garbage Collection erfasst wird. Der folgende einfache Datentyp Data ist ein einfacher Wrapper eines int-Werts:

internal partial class Data
{
    private readonly int value;

    static Data() // Static constructor.
    {
        Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // .cctor
    }

    internal Data(int value) // Constructor.
    {
        Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // .ctor
        this.value = value;
    }

    internal int Value
    {
        get { return this.value; }
    }

    ~Data() // Finalizer.
    {
        Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // Finalize
    }
    // Compiled to:
    // protected override void Finalize()
    // {
    //    try
    //    {
    //        Trace.WriteLine(MethodBase.GetCurrentMethod().Name);
    //    }
    //    finally
    //    {
    //        base.Finalize();
    //    }
    // }
}

Hier gibt die statische GetCurrentMethod-Methode von System.Reflection.MethodBase eine System.Reflection.MethodInfo-Instanz zurück, um den aktuell ausgeführten Funktionsmember darzustellen. Die Name-Eigenschaft von MethodInfo gibt den tatsächlichen Funktionsnamen zur Laufzeit zurück. Der statische Konstruktor wird zu einer statischen Methode wie Member kompiliert, die parameterlos ist und void zurückgibt und einen speziellen Namen .cctor (Klassenkonstruktor) hat. Der Konstruktor wird zu einer Instanzmethode wie Member kompiliert, mit dem speziellen Namen .ctor (Konstruktor). Und Finalizer wird zu einer geschützten Instanzmethode Finalize kompiliert, die auch die Finalize-Methode des Basistyps aufruft.

Statische Methode und Instanzmethode

Nehmen Sie immer noch den obigen Datentyp als Beispiel. Instanzmethode und statische Methode und im Typ:

definiert sein
internal partial class Data
{
    internal int InstanceAdd(int value1, int value2)
    {
        return this.value + value1 + value2;
    }

    internal static int StaticAdd(Data @this, int value1, int value2)
    {
        return @this.value + value1 + value2;
    }
}

Diese 2 Methoden fügen beide das Wertfeld einer Dateninstanz mit anderen Ganzzahlen hinzu. Der Unterschied besteht darin, dass die statische Methode dieses Schlüsselwort nicht verwenden kann, um auf die Data-Instanz zuzugreifen, sodass eine Data-Instanz als erster Parameter an die statische Methode übergeben wird. Diese 2 Methoden sind mit unterschiedlicher Signatur, aber identischer CIL in ihren Körpern kompiliert:

.method assembly hidebysig instance int32 InstanceAdd (
    int32 value1,
    int32 value2) cil managed 
{
    .maxstack  2
    .locals init ([0] int32 V_0) // Local int variable V_0.
    IL_0000:  nop // Do nothing.
    IL_0001:  ldarg.0 // Load first argument this.
    IL_0002:  ldfld int32 Data::'value' // Load field this.value.
    IL_0007:  ldarg.1 // Load second argument value1.
    IL_0008:  add // Add this.value and value1.
    IL_0009:  ldarg.2 // Load third argument value2.
    IL_000a:  add // Add value2.
    IL_000b:  stloc.0 // Set result to first local variable V_0.
    IL_000c:  br.s IL_000e // Transfer control to IL_000e.
    IL_000e:  ldloc.0 // Load first local variable V_0.
    IL_000f:  ret // Return V_0.
}

.method assembly hidebysig static int32 StaticAdd (
    class Data this,
    int32 value1,
    int32 value2) cil managed 
{
    .maxstack  2
    .locals init ([0] int32 V_0) // Local int variable V_0.
    IL_0000:  nop // Do nothing.
    IL_0001:  ldarg.0 // Load first argument this.
    IL_0002:  ldfld int32 Data::'value' // Load field this.value.
    IL_0007:  ldarg.1 // Load second argument value1.
    IL_0008:  add // Add this.value and value1.
    IL_0009:  ldarg.2 // Load third argument value2.
    IL_000a:  add // Add value2.
    IL_000b:  stloc.0 // Set result to first local variable V_0.
    IL_000c:  br.s IL_000e // Transfer control to IL_000e.
    IL_000e:  ldloc.0 // Load first local variable V_0.
    IL_000f:  ret // Return V_0.
}

Intern funktioniert die Instanzmethode also ähnlich wie die statische Methode. Der Unterschied besteht darin, dass in einer Instanzmethode die aktuelle Instanz, auf die mit diesem Schlüsselwort verwiesen werden kann, zum ersten tatsächlichen Argument wird, das erste deklarierte Argument aus der Methodensignatur zum zweiten tatsächlichen Argument wird, das zweite deklarierte Argument zum dritten tatsächlichen Argument wird , usw. Die Ähnlichkeit der obigen Instanz- und statischen Methoden kann wie folgt angezeigt werden:

internal int CompiledInstanceAdd(int value1, int value2)
{
    Data arg0 = this;
    int arg1 = value1;
    int arg2 = value2;
    return arg0.value + arg1 + arg2;
}

internal static int CompiledStaticAdd(Data @this, int value1, int value2)
{
    Data arg0 = @this;
    int arg1 = value1;
    int arg2 = value2;
    return arg0.value + arg1 + arg2;
}

Erweiterungsmethode

C# 3.0 führt die Erweiterungsmethode syntaktischer Zucker ein. Eine Erweiterungsmethode ist eine statische Methode, die in einer statischen, nicht generischen Klasse definiert ist, wobei dieses Schlüsselwort dem ersten Parameter vorangeht:

internal static partial class DataExtensions
{
    internal static int ExtensionAdd(this Data @this, int value1, int value2)
    {
        return @this.Value + value1 + value2;
    }
}

Die obige Methode wird als Erweiterungsmethode für den Datentyp bezeichnet. Sie kann wie eine Instanzmethode vom Datentyp aufgerufen werden:

internal static void CallExtensionMethod(Data data)
{
    int result = data.ExtensionAdd(1, 2L);
}

Das erste deklarierte Argument der Erweiterungsmethode wird also zur aktuellen Instanz, das zweite deklarierte Argument wird zum ersten aufrufenden Argument, das dritte deklarierte Argument wird zum zweiten aufrufenden Argument und so weiter. Dieses Syntaxdesign ist basierend auf der Natur von Instanz- und statischen Methoden leicht verständlich. Tatsächlich wird die Erweiterungsmethodendefinition mit System.Runtime.CompilerServices.ExtensionAttribute:

zu einer normalen statischen Methode kompiliert
internal static partial class DataExtensions
{
    [Extension]
    internal static int CompiledExtensionAdd(Data @this, int value1, int value2)
    {
        return @this.Value + value1 + value2;
    }
}

Und der Methodenaufruf der Erweiterung wird zum normalen statischen Methodenaufruf kompiliert:

internal static void CompiledCallExtensionMethod(Data data)
{
    int result = DataExtensions.ExtensionAdd(data, 1, 2L);
}

Wenn sowohl eine echte Instanzmethode als auch ein Erweiterungsname für denselben Typ mit äquivalenter Signatur definiert sind:

internal partial class Data : IEquatable<Data>
{
    public override bool Equals(object obj)
    {
        return obj is Data && this.Equals((Data)obj);
    }

    public bool Equals(Data other) // Member of IEquatable<T>.
    {
        return this.value == other.value;
    }
}

internal static partial class DataExtensions
{
    internal static bool Equals(Data @this, Data other)
    {
        return @this.Value == other.Value;
    }
}

Der Methodenaufruf im Instanzstil wird in den Methodenaufruf der Instanz kompiliert; Um die Erweiterungsmethode aufzurufen, verwenden Sie die statische Methodenaufrufsyntax:

internal static partial class Functions
{
    internal static void CallMethods(Data data1, Data data2)
    {
        bool result1 = data1.Equals(string.Empty); // object.Equals.
        bool result2 = data1.Equals(data2); // Data.Equals.
        bool result3 = DataExtensions.Equals(data1, data2); // DataExtensions.Equals.
    }
}

Beim Kompilieren von Methodenaufrufen im Instanzstil sucht der C#-Compiler nach Methoden in der folgenden Reihenfolge:

  • Instanzmethode, die im Typ definiert ist
  • Erweiterungsmethode im aktuellen Namensraum definiert
  • Erweiterungsmethode, die in den übergeordneten Namespaces des aktuellen Namespace definiert ist
  • Erweiterungsmethode, die in den anderen Namespaces definiert ist, die mithilfe von Direktiven importiert wurden

Die Erweiterungsmethode kann so betrachtet werden, als ob die Instanzmethode dem angegebenen Typ „hinzugefügt“ worden wäre. Beispielsweise können Aufzählungstypen, wie bereits erwähnt, keine Methoden haben. Für den Aufzählungstyp kann jedoch eine Erweiterungsmethode definiert werden:

internal static class DayOfWeekExtensions
{
    internal static bool IsWeekend(this DayOfWeek dayOfWeek)
    {
        return dayOfWeek == DayOfWeek.Sunday || dayOfWeek == DayOfWeek.Saturday;
    }
}

Jetzt kann die obige Erweiterungsmethode so aufgerufen werden, als wäre sie die Instanzmethode des Aufzählungstyps:

internal static void CallEnumerationExtensionMethod(DayOfWeek dayOfWeek)
{
    bool result = dayOfWeek.IsWeekend();
}

Die meisten LINQ-Abfragemethoden sind Erweiterungsmethoden, wie die zuvor gezeigten Methoden Where, OrderBy, Select:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TSource> Where<TSource>(
            this IEnumerable<TSource> source, Func<TSource, bool> predicate);

        public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
            this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);

        public static IEnumerable<TResult> Select<TSource, TResult>(
            this IEnumerable<TSource> source, Func<TSource, TResult> selector);
    }
}

Die Verwendung und Implementierung dieser Methoden wird im Kapitel LINQ to Objects ausführlich behandelt.

Dieses Tutorial verwendet die folgenden Erweiterungsmethoden, um die Ablaufverfolgung von Einzelwerten und Werten in Folge zu vereinfachen:

public static class TraceExtensions
{
    public static T WriteLine<T>(this T value)
    {
        Trace.WriteLine(value);
        return value;
    }

    public static T Write<T>(this T value)
    {
        Trace.Write(value);
        return value;
    }

    public static IEnumerable<T> WriteLines<T>(this IEnumerable<T> values, Func<T, string> messageFactory = null)
    {
        if (messageFactory!=null)
        {
            foreach (T value in values)
            {
                string message = messageFactory(value);
                Trace.WriteLine(message);
            }
        }
        else
        {
            foreach (T value in values)
            {
                Trace.WriteLine(value);
            }
        }
        return values;
    }
}

Die WriteLine- und Write-Erweiterungsmethoden sind für jeden Wert verfügbar, und WriteLines ist für jede IEnumerable-Sequenz verfügbar:

internal static void TraceValueAndSequence(Uri value, IEnumerable<Uri> values)
{
    value.WriteLine();
    // Equivalent to: Trace.WriteLine(value);

    values.WriteLines();
    // Equivalent to: 
    // foreach (Uri value in values)
    // {
    //    Trace.WriteLine(value);
    // }
}

Mehr benannte Funktionen

C# unterstützt die Operatorüberladung und Typkonvertierungsoperatoren werden definiert, sie werden zu statischen Methoden kompiliert. Zum Beispiel:

internal partial class Data
{
    public static Data operator +(Data data1, Data data2)
    // Compiled to: public static Data op_Addition(Data data1, Data data2)
    {
        Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // op_Addition
        return new Data(data1.value + data2.value);
    }

    public static explicit operator int(Data value)
    // Compiled to: public static int op_Explicit(Data data)
    {
        Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // op_Explicit
        return value.value;
    }

    public static explicit operator string(Data value)
    // Compiled to: public static string op_Explicit(Data data)
    {
        Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // op_Explicit
        return value.value.ToString();
    }

    public static implicit operator Data(int value)
    // Compiled to: public static Data op_Implicit(int data)
    {
        Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // op_Implicit
        return new Data(value);
    }
}

Die Operatorüberladung + wird in die statische Methode mit dem Namen op_Addition kompiliert, die expliziten/impliziten Typkonvertierungen werden in die statischen Methoden op_Explicit/op_Implicit method kompiliert. Die Verwendung dieser Operatoren wird zu statischen Methodenaufrufen kompiliert:

internal static void Operators(Data data1, Data data2)
{
    Data result = data1 + data2; // Compiled to: Data.op_Addition(data1, data2)
    int int32 = (int)data1; // Compiled to: Data.op_Explicit(data1)
    string @string = (string)data1; // Compiled to: Data.op_Explicit(data1)
    Data data = 1; // Compiled to: Data.op_Implicit(1)
}

Beachten Sie, dass die beiden obigen op_Explicit-Methoden der Sonderfall des Ad-hoc-Polymorphismus (Methodenüberladung) in C# sind.

Getter und Setter von Eigenschaftsmitgliedern werden ebenfalls zu benannten Methoden kompiliert. Zum Beispiel:

internal partial class Device
{
    private string description;

    internal string Description
    {
        get // Compiled to: internal string get_Description()
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // get_Description
            return this.description;
        }
        set // Compiled to: internal void set_Description(string value)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // set_Description
            this.description = value;
        }
    }
}

Die Getter- und Setter-Aufrufe der Eigenschaften werden zu Methodenaufrufen kompiliert:

internal static void Property(Device device)
{
    string description = device.Description; // Compiled to: device.get_Description()
    device.Description = string.Empty; // Compiled to: device.set_Description(string.Empty)
}

Indexer-Member kann als parametrisierte Eigenschaft angezeigt werden. Die Indexer-Getter/Setter werden immer in get_Item/set_Item-Methoden kompiliert:

internal partial class Category
{
    private readonly Subcategory[] subcategories;

    internal Category(Subcategory[] subcategories)
    {
        this.subcategories = subcategories;
    }

    internal Subcategory this[int index]
    {
        get // Compiled to: internal Uri get_Item(int index)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // get_Item
            return this.subcategories[index];
        }
        set // Compiled to: internal Uri set_Item(int index, Subcategory subcategory)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // set_Item
            this.subcategories[index] = value;
        }
    }
}

internal static void Indexer(Category category)
{
    Subcategory subcategory = category[0]; // Compiled to: category.get_Item(0)
    category[0] = subcategory; // Compiled to: category.set_Item(0, subcategory)
}

Wie bereits erwähnt, hat ein Ereignis einen Add-Accessor und einen Remove-Accessor, die entweder benutzerdefiniert sind oder vom Compiler generiert werden. Sie werden auch zu benannten Methoden kompiliert:

internal partial class Data
{
    internal event EventHandler Saved
    {
        add // Compiled to: internal void add_Saved(EventHandler value)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // add_Saved
        }
        remove // Compiled to: internal void remove_Saved(EventHandler value)
        {
            Trace.WriteLine(MethodBase.GetCurrentMethod().Name); // remove_Saved
        }
    }
}

Ereignis ist eine Funktionsgruppe. Die Operatoren +=/-=fügen dem Ereignis die Event-Handler-Funktion hinzu, und der Operator –=entfernt die Event-Handler-Funktion aus dem Ereignis. Sie werden zu den Aufrufen der oben genannten Methoden kompiliert:

internal static void DataSaved(object sender, EventArgs args) { }

internal static void EventAccessor(Data data)
{
    data.Saved += DataSaved; // Compiled to: data.add_Saved(DataSaved)
    data.Saved -= DataSaved; // Compiled to: data.remove_Saved(DataSaved)
}

Das Ereignis von C# wird ausführlich im Delegate-Kapitel behandelt.

Funktionspolymorphismen

Das Wort „Polymorphismus“ kommt aus dem Griechischen und bedeutet „viele Formen“. Beim Programmieren gibt es mehrere Arten von Polymorphismen. In der objektorientierten Programmierung kann ein abgeleiteter Typ die bereitzustellenden Methoden des Basistyps überschreiben. Beispielsweise werden der Typ System.IO.FileStream und der Typ System.IO.Memory vom Typ System.IO.Stream abgeleitet:

namespace System.IO
{
    public abstract class Stream : MarshalByRefObject, IDisposable
    {
        public virtual void WriteByte(byte value);
    }

    public class FileStream : Stream
    {
        public override void WriteByte(byte value);
    }

    public class MemoryStream : Stream
    {
        public override void WriteByte(byte value);
    }
}

FileStream.WriteByte überschreibt Stream.WriteByte, um das Schreiben in das Dateisystem zu implementieren, und MemoryStream.WriteByte überschreibt Stream.WriteByte, um das Schreiben in den Arbeitsspeicher zu implementieren. Dies wird als Subtyp-Polymorphismus oder Inklusionspolymorphismus bezeichnet. In der objektorientierten Programmierung bezieht sich der Begriff Polymorphismus normalerweise auf Subtyp-Polymorphismus. Es gibt auch Ad-hoc-Polymorphismus und parametrischen Polymorphismus. In der funktionalen Programmierung bezieht sich der Begriff Polymorphismus normalerweise auf parametrischen Polymorphismus.

Ad-hoc-Polymorphismus:Methodenüberladung

Durch das Überladen von Methoden können mehrere Methoden denselben Methodennamen mit unterschiedlichen Parameternummern und/oder -typen haben. Zum Beispiel:

namespace System.Diagnostics
{
    public sealed class Trace
    {
        public static void WriteLine(string message);

        public static void WriteLine(object value);
    }
}

Anscheinend schreibt die WriteLine-Überladung für String die String-Nachricht. Wenn dies die einzige bereitgestellte Methode ist, müssen alle Nicht-String-Werte manuell in eine String-Darstellung konvertiert werden:

internal partial class Functions
{
    internal static void TraceString(Uri uri, FileInfo file, int int32)
    {
        Trace.WriteLine(uri?.ToString());
        Trace.WriteLine(file?.ToString());
        Trace.WriteLine(int32.ToString());
    }
}

Die WriteLine-Überladung für das Objekt bietet Komfort für Werte beliebiger Typen. Der obige Code kann vereinfacht werden zu:

internal static void TraceObject(Uri uri, FileInfo file, int int32)
{
    Trace.WriteLine(uri);
    Trace.WriteLine(file);
    Trace.WriteLine(int32);
}

Bei mehreren Überladungen ist die WriteLine-Methode polymorph und kann mit unterschiedlichen Argumenten aufgerufen werden. Dies wird als Ad-hoc-Polymorphismus bezeichnet. In der .NET Core-Bibliothek ist die Methode ToString von System.Convert die polymorphe Ad-hoc-Methode. Es verfügt über 36 Überladungen, um Werte verschiedener Typen auf unterschiedliche Weise in eine Zeichenfolgendarstellung umzuwandeln:

namespace System
{
    public static class Convert
    {
        public static string ToString(bool value);

        public static string ToString(int value);

        public static string ToString(long value);

        public static string ToString(decimal value);

        public static string ToString(DateTime value);

        public static string ToString(object value);

        public static string ToString(int value, IFormatProvider provider);

        public static string ToString(int value, int toBase);

        // More overloads and other members.
    }
}

In C#/.NET können Konstruktoren auch Parameter haben, sodass sie auch überladen werden können. Zum Beispiel:

namespace System
{
    public struct DateTime : IComparable, IFormattable, IConvertible, IComparable<DateTime>, IEquatable<DateTime>
    {
        public DateTime(long ticks);

        public DateTime(int year, int month, int day);

        public DateTime(int year, int month, int day, int hour, int minute, int second);

        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond);

        // Other constructor overloads and other members.
    }
}

Indexer sind im Wesentlichen get_Item/set_Item-Methoden mit Parametern, sodass sie auch überladen werden können. Nehmen Sie als Beispiel System.Data.DataRow:

namespace System.Data
{
    public class DataRow
    {
        public object this[DataColumn column] { get; set; }

        public object this[string columnName] { get; set; }

        public object this[int columnIndex] { get; set; }

        // Other indexer overloads and other members.
    }
}

C# erlaubt keine Methodenüberladung mit nur unterschiedlichem Rückgabetyp. Das folgende Beispiel kann nicht kompiliert werden:

internal static string FromInt64(long value)
{
    return value.ToString();
}

internal static DateTime FromInt64(long value)
{
    return new DateTime(value);
}

Dafür gibt es eine Ausnahme. Im obigen Beispiel werden zwei explizite Typkonvertierungsoperatoren beide mit einem einzigen Data-Parameter in op_Explicit-Methoden kompiliert. Eine op_Explicit-Methode gibt einen Int zurück, die andere op_Explicit-Methode gibt einen String zurück. Dies ist der einzige Fall, in dem C# eine Methodenüberladung nur mit unterschiedlichen Rückgabetypen zulässt.

Parametrischer Polymorphismus:generische Methode

Neben Ad-hoc-Polymorphismus unterstützt C# seit 2.0 auch parametrischen Polymorphismus für Methoden. Das Folgende ist eine normale Methode, die 2 Int-Werte vertauscht:

internal static void SwapInt32(ref int value1, ref int value2)
{
    (value1, value2) = (value2, value1);
}

Die obige Syntax wird als Tupelzuweisung bezeichnet, die ein neues Feature von C# 7.0 ist und im Tupelkapitel behandelt wird. Um diesen Code für Werte eines beliebigen anderen Typs wiederzuverwenden, definieren Sie einfach eine generische Methode, indem Sie int durch einen Typparameter ersetzen. Ähnlich wie bei generischen Typen werden auch die Typparameter generischer Methoden in spitzen Klammern nach dem Methodennamen deklariert:

internal static void Swap<T>(ref T value1, ref T value2)
{
    (value1, value2) = (value2, value1);
}

Die Einschränkungssyntax des generischen Typparameters funktioniert auch für generische Methoden. Zum Beispiel:

internal static IStack<T> PushValue<T>(IStack<T> stack) where T : new()
{
    stack.Push(new T());
    return stack;
}

Sowohl generische Typen als auch generische Methoden werden in der funktionalen Programmierung in C# häufig verwendet. Beispielsweise ist fast jede LINQ-Abfrage-API parametrisch polymorph.

Typ-Argument-Inferenz

Wenn der C#-Compiler beim Aufrufen einer generischen Methode alle Typargumente der generischen Methode ableiten kann, können die Typargumente zur Entwurfszeit weggelassen werden. Zum Beispiel

internal static void TypeArgumentInference(string value1, string value2)
{
    Swap<string>(ref value1, ref value2);
    Swap(ref value1, ref value2);
}

Swap wird mit Zeichenfolgenwerten aufgerufen, sodass der C#-Compiler auf das Typargument Zeichenfolge schließt und an den Typparameter T der Methode übergeben wird. Der C#-Compiler kann Typargumente nur vom Typ der Argumente ableiten, nicht vom Typ des Rückgabewerts. Nehmen Sie die folgenden generischen Methoden als Beispiel:

internal static T Generic1<T>(T value)
{
    Trace.WriteLine(value);
    return default(T);
}

internal static TResult Generic2<T, TResult>(T value)
{
    Trace.WriteLine(value);
    return default(TResult);
}

Beim Aufruf kann das Typargument von Generic1 weggelassen werden, aber die Typargumente von Generic2 nicht:

internal static void ReturnTypeInference()
{
    int value1 = Generic1(0);
    string value2 = Generic2<int, string>(0); // Generic2<int>(0) cannot be compiled.
}

Für Generic1 wird T als Rückgabetyp verwendet, kann aber aus dem Argumenttyp abgeleitet werden. Daher kann das Typargument für Generic1 weggelassen werden. Für Generic2 kann T auch vom Argumenttyp abgeleitet werden, aber TResult kann möglicherweise nur vom Typ des Rückgabewerts abgeleitet werden, was vom C#-Compiler nicht unterstützt wird. Daher können Typargumente beim Aufruf von Generic2 nicht weggelassen werden. Andernfalls gibt der C#-Compiler den Fehler CS0411 aus:Die Typargumente für die Methode „Functions.Generic2(T)“ können nicht aus der Verwendung abgeleitet werden. Versuchen Sie, die Typargumente explizit anzugeben.

Der Typ kann nicht von null abgeleitet werden, da null ein beliebiger Verweistyp oder nullfähiger Werttyp sein kann. Zum Beispiel beim Aufrufen von Generic1 oben mit null:

internal static void NullArgumentType()
{
    Generic1<FileInfo>(null);
    Generic1((FileInfo)null);
    FileInfo file = null;
    Generic1(file);
}

Es gibt einige Optionen:

  • Geben Sie das Typargument an
  • Null explizit in den erwarteten Argumenttyp konvertieren
  • Erstellen Sie eine temporäre Variable des erwarteten Argumenttyps, übergeben Sie den Wert an die generische Methode

Typargumentrückschluss wird für den Konstruktor des generischen Typs nicht unterstützt. Nehmen Sie den folgenden generischen Typ als Beispiel:

internal class Generic<T>
{
    internal Generic(T input) { } // T cannot be inferred.
}

Beim Aufrufen des obigen Konstruktors müssen Typargumente bereitgestellt werden:

internal static Generic<IEnumerable<IGrouping<int, string>>> GenericConstructor(
    IEnumerable<IGrouping<int, string>> input)
{
    return new Generic<IEnumerable<IGrouping<int, string>>>(input);
    // Cannot be compiled:
    // return new Generic(input);
}

Eine Lösung besteht darin, den Konstruktoraufruf in eine statische Factory-Methode einzuschließen, in der der Typparameter abgeleitet werden kann:

internal class Generic // Not Generic<T>.
{
    internal static Generic<T> Create<T>(T input) => new Generic<T>(input); // T can be inferred.
}

Jetzt kann die Instanz ohne Typargument konstruiert werden:

internal static Generic<IEnumerable<IGrouping<int, string>>> GenericCreate(
    IEnumerable<IGrouping<int, string>> input)
{
    return Generic.Create(input);
}

Statischer Import

C# 6.0 führt die using-Static-Direktive, einen syntaktischen Zucker, ein, um den Zugriff auf statische Member des angegebenen Typs zu ermöglichen, sodass eine statische Methode als Typname aufgerufen werden kann, als wäre sie eine spontane Funktion. Da Erweiterungen im Wesentlichen statische Methoden sind, kann diese Syntax auch Erweiterungsmethoden vom angegebenen Typ importieren. Es ermöglicht auch den Zugriff auf Aufzählungsmitglieder ohne Aufzählungstypname.

using static System.DayOfWeek;
using static System.Math;
using static System.Diagnostics.Trace;
using static System.Linq.Enumerable;

internal static partial class Functions
{
    internal static void UsingStatic(int value, int[] array)
    {
        int abs = Abs(value); // Compiled to: Math.Abs(value)
        WriteLine(Monday); // Compiled to: Trace.WriteLine(DayOfWeek.Monday)
        List<int> list = array.ToList(); // Compiled to: Enumerable.ToList(array)
    }
}

Die using-Direktive importiert die angegebenen Erweiterungsmethoden aller Typen unter dem angegebenen Namespace, während die using-Static-Direktive nur die Erweiterungsmethoden des angegebenen Typs importiert.

Teilmethode

Teilmethoden können in Teilklassen oder Teilstrukturen definiert werden. Ein Teil des Typs kann die partielle Methodensignatur aufweisen, und die partielle Methode kann optional in einem anderen Teil des Typs implementiert werden. Dieser syntaktische Zucker ist nützlich für die Codegenerierung. Beispielsweise kann LINQ to SQL den Entitätstyp nach folgendem Muster generieren:

[Table(Name = "Production.Product")]
public partial class Product : INotifyPropertyChanging, INotifyPropertyChanged
{
    public Product()
    {
        this.OnCreated(); // Call.
    }

    partial void OnCreated(); // Signature.

    // Other members.
}

Der Konstruktor ruft die partielle Methode OnCreate auf, die ein Hook ist. Bei Bedarf kann der Entwickler einen weiteren Teil des Entitätstyps bereitstellen, um OnCreate zu implementieren:

public partial class Product
{
    partial void OnCreated() // Optional implementation.
    {
        Trace.WriteLine($"{nameof(Product)} is created.");
    }
}

Wenn eine partielle Methode implementiert wird, wird sie zu einer normalen privaten Methode kompiliert. Wenn eine partielle Methode nicht implementiert ist, ignoriert der Compiler die Methodensignatur und entfernt alle Methodenaufrufe. Aus diesem Grund sind Zugriffsmodifikatoren (wie public usw.), Attribute und nicht-void-Rückgabewerte für partielle Methoden nicht erlaubt.