Funktionale C#-Programmierung im Detail (1) Grundlagen der C#-Sprache

Funktionale C#-Programmierung im Detail (1) Grundlagen der C#-Sprache

[LINQ via C#-Reihe]

[Eingehende Serie zur funktionalen Programmierung in C#]

Neueste Version: https://weblogs.asp.net/dixin/functional-csharp-fundamentals

C# 1.0 wurde ursprünglich im Jahr 2002 veröffentlicht, wie die erste Sprachspezifikation zu Beginn sagt, C# ist eine „einfache, moderne, objektorientierte und typsichere“ Programmiersprache für allgemeine Zwecke. Jetzt hat sich C# zu 7.2 weiterentwickelt. Im Laufe der Jahre wurden viele großartige Sprachfeatures, insbesondere reichhaltige funktionale Programmierfeatures, zu C# hinzugefügt. Jetzt ist die C#-Sprache produktiv und elegant, imperativ und deklarativ, objektorientiert und funktional. Mit Frameworks wie .NET Framework, .NET Core, Mono, Xamarin, Unity usw. wird C# von Millionen von Menschen auf verschiedenen Plattformen verwendet, darunter Windows, Linux, Mac, iOS, Android usw.

Dieses Tutorial ist vollständig für die Sprache C# und konzentriert sich auf ihre funktionalen Aspekte. Es wird davon ausgegangen, dass die Leser über die allgemeinen Konzepte zur Programmierung und C#-Sprache verfügen. Dieses Kapitel gibt einen Überblick über die grundlegenden, aber wichtigen Elemente und die Syntax von C# 1.0 bis 7.x, um sowohl Anfänger als auch Leser aufzuwärmen, die noch nicht mit einigen neuen Syntaxen vertraut sind, die in den letzten C#-Releases eingeführt wurden. Die anderen erweiterten Funktionen und Konzepte werden in späteren Kapiteln ausführlich besprochen. Dieses Tutorial behandelt nicht die Themen und Sprachfunktionen außerhalb des Bereichs der funktionalen Programmierung und LINQ, wie z. B. Vererbung von objektorientierter Programmierung, Zeiger in unsicherem Code, Interoperabilität mit anderem nicht verwaltetem Code, dynamische Programmierung usw.

C# Funktionen in diesem Kapitel Features in anderen Kapiteln Nicht abgedeckte Funktionen
1.0 Klasse
Struktur
Schnittstelle
Aufzählung
using-Anweisung
Delegierter
Vorfall
Funktionsmitglied
ref-Parameter
out-Parameter
Parameter-Array
für jede Anweisung
Vererbung
Zeiger
Interoperabilität
1.1 Pragma-Direktive
1.2 foreach für IDisposable
2.0 Statische Klasse
Partielle Art
Allgemeiner Typ
Nullable-Werttyp
Null-Koaleszenz-Operator
Anonyme Methode
Generator
Kovarianz und Kontravarianz
Generische Methode
3.0 Auto-Eigenschaft
Objektinitialisierer
Sammlungsinitialisierer
Anonymer Typ
Implizit typisierte lokale Variable
Abfrageausdruck
Lambda-Ausdruck
Erweiterungsmethode
Partielle Methode
4.0 Benanntes Argument
Optionales Argument
Generische Kovarianz und Kontravarianz
Dynamische Bindung
5.0 Asynchrone Funktion
Anruferinfo-Argument
6.0 Eigenschaftsinitialisierer
Wörterbuch-Initialisierer
Null-Fortpflanzungsoperator
Ausnahmefilter
String-Interpolation
Name des Betreibers
Statischer Import
Mitglied mit Ausdruckskörper
im catch/finally-Block warten
7.0 throw-Ausdruck
Zifferntrennzeichen
Out-Variable
Tupel und Dekonstruktion
Lokale Funktion
Körperelement des erweiterten Ausdrucks
ref return und local
Verwerfen
Verallgemeinerte asynchrone Rückgabe
Ausdruck werfen
Musterabgleich
7.1 Standard-Literalausdruck Async Main-Methode
Abgeleiteter Name des Tupelelements
7.2 ref-Struktur
Führende Unterstriche in numerischen Literalen
Nicht abschließende benannte Argumente
im Parameter
ref readonly return und local
Schreibgeschützte Struktur
privat geschützter Modifikator

Typen und Mitglieder

C# ist stark typisiert. In C# hat jeder Wert einen Typ. C# unterstützt 5 Arten von Typen:Klasse, Struktur, Aufzählung, Delegat und Schnittstelle.

Eine Klasse ist ein Referenztyp, der mit dem Schlüsselwort class definiert wird. Es kann Felder, Eigenschaften, Methoden, Ereignisse, Operatoren, Indexer, Konstruktoren, Destruktoren und verschachtelte Klassen-, Struktur-, Enumerations-, Delegat- und Schnittstellentypen haben. Eine Klasse wird immer von System.Object abgeleitet Klasse.

namespace System
{
    public class Object
    {
        public Object();

        public static bool Equals(Object objA, Object objB);

        public static bool ReferenceEquals(Object objA, Object objB);

        public virtual bool Equals(Object obj);

        public virtual int GetHashCode();

        public Type GetType();

        public virtual string ToString();

        // Other members.
    }
}

Object verfügt über eine statische Equals-Methode zum Testen, ob zwei Instanzen als gleich angesehen werden, eine Instanz-Equals-Methode zum Testen, ob die aktuelle Instanz und die andere Instanz als gleich angesehen werden, und eine statische ReferenceEquals-Methode zum Testen, ob zwei Instanzen dieselbe Instanz sind. Es hat eine GetHashCode-Methode als Standard-Hash-Funktion, um eine Hash-Code-Nummer für einen schnellen Test von Instanzen zurückzugeben. Es hat auch eine GetType-Methode, um den Typ der aktuellen Instanz zurückzugeben, und eine ToString-Methode, um die Textdarstellung der aktuellen Instanz zurückzugeben.

Das folgende Beispiel ist ein Segment der System.Exception-Klassenimplementierung in .NET Framework. Es demonstriert die Syntax zum Definieren einer Klasse und verschiedener Arten von Membern. Diese Klasse implementiert die System.ISerializable-Schnittstelle und leitet die System._Exception-Klasse ab. Beim Definieren einer Klasse kann die Basisklasse System.Object weggelassen werden.

namespace System
{
    [Serializable]
    public class Exception : ISerializable, _Exception // , System.Object
    {
        internal string _message; // Field.
        
        private Exception _innerException; // Field.

        [OptionalField(VersionAdded = 4)]
        private SafeSerializationManager _safeSerializationManager; // Field.

        public Exception InnerException { get { return this._innerException; } } // Property.

        public Exception(string message, Exception innerException) // Constructor.
        {
            this.Init();
            this._message = message;
            this._innerException = innerException;
        }

        public virtual Exception GetBaseException() // Method.
        {
            Exception innerException = this.InnerException;
            Exception result = this;
            while (innerException != null)
            {
                result = innerException;
                innerException = innerException.InnerException;
            }
            return result;
        }

        protected event EventHandler<SafeSerializationEventArgs> SerializeObjectState // Event.
        {
            add
            {
                this._safeSerializationManager.SerializeObjectState += value;
            }
            remove
            {
                this._safeSerializationManager.SerializeObjectState -= value;
            }
        }

        internal enum ExceptionMessageKind // Nested enumeration type.
        {
            ThreadAbort = 1,
            ThreadInterrupted = 2,
            OutOfMemory = 3
        }

        // Other members.
    }
}

Eine Struktur ist ein mit dem Schlüsselwort struct definierter Werttyp, der dann von System.Object abgeleitet wird Klasse. Es kann alle Arten von Klassenmitgliedern außer dem Destruktor haben. Eine Struktur leitet sich immer von System.ValueType ab Klasse, und interessanterweise ist System.ValueType ein Referenztyp, der von System.Object abgeleitet ist. In der Praxis wird eine Struktur normalerweise so definiert, dass sie eine sehr kleine und unveränderliche Datenstruktur darstellt, um die Leistung der Speicherzuordnung/Zuordnungsfreigabe zu verbessern. Zum Beispiel die . Im .NET Core-System. ist implementiert als:

namespace System
{
    public struct TimeSpan : IComparable, IComparable<TimeSpan>, IEquatable<TimeSpan>, IFormattable // , System.ValueType
    {
        public const long TicksPerMillisecond = 10000; // Constant.

        public static readonly TimeSpan Zero = new TimeSpan(0); // Field.

        internal long _ticks; // Field.

        public TimeSpan(long ticks) // Constructor.
        {
            this._ticks = ticks;
        }

        public long Ticks { get { return _ticks; } } // Property.

        public int Milliseconds // Property.
        {
            get { return (int)((_ticks / TicksPerMillisecond) % 1000); }
        }

        public static bool Equals(TimeSpan t1, TimeSpan t2) // Method.
        {
            return t1._ticks == t2._ticks;
        }

        public static bool operator ==(TimeSpan t1, TimeSpan t2) // Operator.
        {
            return t1._ticks == t2._ticks;
        }

        // Other members.
    }
}

Eine Enumeration ist ein Werttyp, der von der System.Enum-Klasse abgeleitet ist, die wiederum von der System.ValueType-Klasse abgeleitet ist. Es kann nur konstante Felder des angegebenen zugrunde liegenden ganzzahligen Typs haben (int standardmäßig). Zum Beispiel:

namespace System
{
    [Serializable]
    public enum DayOfWeek // : int
    {
        Sunday = 0,
        Monday = 1,
        Tuesday = 2,
        Wednesday = 3,
        Thursday = 4,
        Friday = 5,
        Saturday = 6,
    }
}

Ein Delegat ist ein Referenztyp, der von System.MulticastDelegate abgeleitet ist Klasse, die von System.Delegate abgeleitet ist Klasse. Der Delegattyp stellt den Funktionstyp dar und wird ausführlich im Kapitel zur funktionalen Programmierung behandelt.

namespace System
{
    public delegate void Action();
}

Eine Schnittstelle ist ein Vertrag, der von einer Klasse oder Struktur implementiert werden soll. Schnittstellen können nur öffentliche und abstrakte Eigenschaften, Methoden und Ereignisse ohne Implementierung haben. Zum Beispiel:

namespace System.ComponentModel
{
    public interface INotifyDataErrorInfo
    {
        event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; // Event.

        bool HasErrors { get; } // Property.

        IEnumerable GetErrors(string propertyName); // Method.
    }
}

Jede Klasse oder Struktur, die die obige Schnittstelle implementiert, muss die angegebenen 3 Mitglieder als öffentlich haben.

Eingebaute Typen

Es gibt grundlegende. NET-Typen, die am häufigsten in der C#-Programmierung verwendet werden, daher stellt C# Sprachschlüsselwörter als Aliase dieser Typen bereit, die als integrierte Typen von C# bezeichnet werden:

C#-Schlüsselwort .NET-Typ
bool System.Boolean
sbyte System.SByte
Byte System.Byte
Zeichen System.Char
kurz System.Init16
ushort System.UInit16
int System.Init32
uint System.UInit32
lang System.Init54
ulong System.UInit54
schwimmen System.Single
doppelt System.Double
dezimal System.Dezimalzahl
Objekt System.Objekt
Zeichenfolge System.String

Referenztyp vs. Werttyp

In C#/.NET sind Klassen Referenztypen, einschließlich Objekt, Zeichenfolge, Array usw.. Delegaten sind ebenfalls Referenztypen, die später besprochen werden. Strukturen sind Werttypen, einschließlich primitiver Typen (bool , sbyte , Byte , char , kurz , ukurz , int , uint , lang , ulong , schwimmen , doppelt ), dezimal , System.DateTime , System.DateTimeOffset , System.TimeSpan , System.Guid , System.Nullable , Enumeration (da der zugrunde liegende Typ einer Enumeration immer ein numerischer primitiver Typ ist) usw. Das folgende Beispiel definiert einen Referenztyp und einen Werttyp, die einander ähnlich aussehen:

internal class Point
{
    private readonly int x;

    private readonly int y;

    internal Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    internal int X { get { return this.x; } }

    internal int Y { get { return this.y; } }
}

internal readonly struct ValuePoint
{
    private readonly int x;

    private readonly int y;

    internal ValuePoint(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    internal int X { get { return this.x; } }

    internal int Y { get { return this.y; } }
}

Instanzen von Referenztyp und Werttyp werden unterschiedlich zugeordnet. Der Referenztyp wird immer auf dem verwalteten Heap zugewiesen und durch die Garbage Collection freigegeben. Der Werttyp wird entweder auf dem Stapel zugewiesen und durch das Entladen des Stapels freigegeben, oder er wird inline mit dem Container zugewiesen und freigegeben. Daher kann der Werttyp im Allgemeinen eine bessere Leistung für die Zuweisung und Freigabe haben. Normalerweise kann ein Typ als Werttyp entworfen werden, wenn er klein, unveränderlich und einem primitiven Typ logisch ähnlich ist. Die obige System.TimeSpan Typstruktur eine Zeitdauer darstellt, ist sie als Werttyp konzipiert, da sie nur ein unveränderlicher Wrapper eines langen Werts ist, der Ticks darstellt. Das folgende Beispiel demonstriert diesen Unterschied:

internal static partial class Fundamentals
{
    internal static void ValueTypeReferenceType()
    {
        Point reference1 = new Point(1, 2);
        Point reference2 = reference1;
        Trace.WriteLine(object.ReferenceEquals(reference1, reference2)); // True

        ValuePoint value1 = new ValuePoint(3, 4);
        ValuePoint value2 = value1;
        Trace.WriteLine(object.ReferenceEquals(value1, value2)); // False

        Point[] referenceArray = new Point[] { new Point(5, 6) };
        ValuePoint[] valueArray = new ValuePoint[] { new ValuePoint(7, 8) };
    }
}

Wenn ein Punkt Die Instanz wird als lokale Variable konstruiert, da es sich um einen Verweistyp handelt, wird sie im verwalteten Heap zugewiesen. Seine Felder sind Werttypen, sodass die Felder auch auf dem verwalteten Heap inline zugewiesen werden. Die lokale Variable reference1 kann als Zeiger betrachtet werden, der auf den verwalteten Heap-Speicherort zeigt, der die Daten enthält. Beim Zuweisen von Referenz1 zu Referenz2 , wird der Zeiger kopiert. Also Referenz1 und Referenz2 beide zeigen auf denselben Punkt Instanz im verwalteten Heap. Wenn ValuePoint ist als lokale Variable konstruiert, da es sich um einen Werttyp handelt. es wird im Stack allokiert. Seine Felder werden ebenfalls inline im Stack allokiert. Die lokale Variable value1 enthält die eigentlichen Daten. Beim Zuweisen von Wert1 zu Wert2 , wird die gesamte Instanz kopiert, also value1 und Wert2 sind 2 verschiedene ValuePoint Instanzen im Stack. In C# leitet sich Array immer von der System.Array-Klasse ab und ist ein Referenztyp. Also sind referenceArray und valueArray beide auf dem Heap allokiert, und ihre Elemente sind auch beide auf dem Heap.

Der Referenztyp kann null sein und der Werttyp nicht:

internal static void Default()
{
    Point defaultReference = default(Point);
    Trace.WriteLine(defaultReference is null); // True

    ValuePoint defaultValue = default(ValuePoint);
    Trace.WriteLine(defaultValue.X); // 0
    Trace.WriteLine(defaultValue.Y); // 0
}

Der Standardwert des Referenztyps ist einfach null. Der Standardwerttyp ist eine tatsächliche Instanz, bei der alle Felder mit ihren Standardwerten initialisiert werden. Tatsächlich wird die Initialisierung der obigen lokalen Variablen kompiliert zu:

internal static void CompiledDefault()
{
    Point defaultReference = null;

    ValuePoint defaultValue = new ValuePoint();
}

Eine Struktur hat praktisch immer einen parameterlosen Standardkonstruktor. Das Aufrufen dieses Standardkonstruktors instanziiert die Struktur und setzt alle ihre Felder auf Standardwerte. Hier defaultValue 's int Felder werden auf 0 initialisiert. Wenn ValuePoint ein Referenztypfeld hat, wird das Referenztypfeld auf null initialisiert.

Standard-Literalausdruck

Seit C# 7.1 kann der Typ im Standardwertausdruck weggelassen werden, wenn der Typ abgeleitet werden kann. Daher kann die obige Standardwertsyntax vereinfacht werden zu:

internal static void DefaultLiteralExpression()
{
    Point defaultReference = default;

    ValuePoint defaultValue = default;
}

ref-Struktur

C# 7.2 aktiviert das Schlüsselwort ref für die Strukturdefinition, sodass die Struktur nur auf dem Stack zugewiesen werden kann. Dies kann für leistungskritische Szenarios hilfreich sein, in denen die Speicherzuweisung/-aufhebung auf dem Heap zu Leistungseinbußen führen kann.

internal ref struct OnStackOnly { }

internal static void Allocation()
{
    OnStackOnly valueOnStack = new OnStackOnly();
    OnStackOnly[] arrayOnHeap = new OnStackOnly[10]; // Cannot be compiled.
}

internal class OnHeapOnly
{
    private OnStackOnly fieldOnHeap; // Cannot be compiled.
}

internal struct OnStackOrHeap
{
    private OnStackOnly fieldOnStackOrHeap; // Cannot be compiled.
}

Wie bereits erwähnt, ist ein Array ein Referenztyp, der auf dem Heap zugewiesen wird, sodass der Compiler kein Array mit einer Ref-Struktur zulässt. Eine Instanz der Klasse wird immer auf dem Heap instanziiert, daher kann die Ref-Struktur nicht als ihr Feld verwendet werden. Eine Instanz einer normalen Struktur kann sich auf dem Stack oder dem Heap befinden, also kann die Ref-Struktur auch nicht als ihr Feld verwendet werden.

Statische Klasse

C# 2.0 ermöglicht statisch Modifikator für die Klassendefinition. Nehmen Sie System.Math als Beispiel:

namespace System
{
    public static class Math
    {
        // Static members only.
    }
}

Eine statische Klasse kann nur statische Mitglieder haben und kann nicht instanziiert werden. Die statische Klasse wird zu einer abstrakten versiegelten Klasse kompiliert. In C# wird static häufig verwendet, um eine Reihe statischer Methoden zu hosten.

Teiltyp

C# 2.0 führt den Teil ein Schlüsselwort, um die Definition von Klasse, Struktur oder Schnittstelle zur Entwurfszeit aufzuteilen.

internal partial class Device
{
    private string name;

    internal string Name
    {
        get { return this.name; }
        set { this.name = value; }
    }
}

internal partial class Device
{
    public string FormattedName
    {
        get { return this.name.ToUpper(); }
    }
}

Dies ist gut für die Verwaltung großer Schriften, indem sie in mehrere kleinere Dateien aufgeteilt werden. Teiltypen werden auch häufig bei der Codegenerierung verwendet, sodass Benutzer benutzerdefinierten Code an vom Tool generierte Typen anhängen können. Zur Kompilierzeit werden die mehreren Teile eines Typs zusammengeführt.

Schnittstelle und Implementierung

Wenn ein Typ eine Schnittstelle implementiert, kann dieser Typ jeden Schnittstellenmember entweder implizit oder explizit implementieren. Die folgende Schnittstelle hat 2 Member-Methoden:

internal interface IInterface
{
    void Implicit();

    void Explicit();
}

Und der folgende Typ implementiert diese Schnittstelle:

internal class Implementation : IInterface
{
    public void Implicit() { }

    void IInterface.Explicit() { }
}

Diese Implementierungen Typ hat ein öffentliches Implizit Methode mit derselben Signatur wie IInterface ist implizit -Methode, sodass der C#-Compiler Implementierungen. übernimmt Implizite Methode als Implementierung von IInterface. Implizite Methode. Diese Syntax wird als implizite Schnittstellenimplementierung bezeichnet. Die andere Methode Explicit wird explizit als Interface-Member implementiert, nicht als Member-Methode des Typs Implementations. Das folgende Beispiel zeigt, wie diese Schnittstellenmitglieder verwendet werden:

internal static void InterfaceMembers()
{
    Implementation @object = new Implementation();
    @object.Implicit(); // @object.Explicit(); cannot be compiled.

    IInterface @interface = @object;
    @interface.Implicit();
    @interface.Explicit();
}

Auf ein implizit implementiertes Schnittstellenmember kann von der Instanz des Implementierungstyps und des Schnittstellentyps aus zugegriffen werden, aber auf ein explizit implementiertes Schnittstellenmember kann nur von der Instanz des Schnittstellentyps aus zugegriffen werden. Hier der Variablenname @object und @interface wird das Sonderzeichen @ vorangestellt, weil Objekt und Schnittstelle sind Schlüsselwörter der C#-Sprache und können nicht direkt als Bezeichner verwendet werden.

IDisposable-Schnittstelle und using-Anweisung

Zur Laufzeit verwaltet CLR/CoreCLR den Arbeitsspeicher automatisch. Es weist Speicher für .NET-Objekte zu und gibt den Speicher mit Garbage Collector frei. Ein .NET-Objekt kann auch andere Ressourcen zuweisen, die nicht von CLR/CoreCLR verwaltet werden, wie geöffnete Dateien, Fensterhandles, Datenbankverbindungen usw. .NET bietet einen Standardvertrag für diese Typen:

namespace System
{
    public interface IDisposable
    {
        void Dispose();
    }
}

Ein Typ, der die obige System.IDisposable-Schnittstelle implementiert, muss über eine Dispose-Methode verfügen, die ihre nicht verwalteten Ressourcen beim Aufrufen explizit freigibt. Beispielsweise stellt System.Data.SqlClient.SqlConnection eine Verbindung zu einer SQL-Datenbank dar, implementiert IDisposable und stellt die Dispose-Methode bereit, um die zugrunde liegende Datenbankverbindung freizugeben. Das folgende Beispiel ist das standardmäßige try-finally-Muster, um das IDisposable-Objekt zu verwenden und die Dispose-Methode aufzurufen:

internal static void Dispose(string connectionString)
{
    SqlConnection connection = new SqlConnection(connectionString);
    try
    {
        connection.Open();
        Trace.WriteLine(connection.ServerVersion);
        // Work with connection.
    }
    finally
    {
        if ((object)connection != null)
        {
            ((IDisposable)connection).Dispose();
        }
    }
}

Die Dispose-Methode wird im finally-Block aufgerufen, sodass sichergestellt ist, dass sie aufgerufen wird, selbst wenn eine Ausnahme von den Operationen im try-Block ausgelöst wird oder wenn der aktuelle Thread abgebrochen wird. IDisposable ist weit verbreitet, daher führt C# seit 1.0 einen syntaktischen Zucker der using-Anweisung ein. Der obige Code entspricht:

internal static void Using(string connectionString)
{
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        Trace.WriteLine(connection.ServerVersion);
        // Work with connection.
    }
}

Dies ist zur Entwurfszeit deklarativer, und try-finally wird zur Kompilierzeit generiert. Disposable-Instanzen sollten immer mit dieser Syntax verwendet werden, um sicherzustellen, dass ihre Dispose-Methode auf die richtige Weise aufgerufen wird.

Generischer Typ

C# 2.0 führt die generische Programmierung ein. Die generische Programmierung ist ein Paradigma, das Typparameter unterstützt, sodass Typinformationen später bereitgestellt werden können. Die folgende Stapeldatenstruktur von int Werte ist nicht generisch:

internal interface IInt32Stack
{
    void Push(int value);

    int Pop();
}

internal class Int32Stack : IInt32Stack
{
    private int[] values = new int[0];

    public void Push(int value)
    {
        Array.Resize(ref this.values, this.values.Length + 1);
        this.values[this.values.Length - 1] = value;
    }

    public int Pop()
    {
        if (this.values.Length == 0)
        {
            throw new InvalidOperationException("Stack empty.");
        }
        int value = this.values[this.values.Length - 1];
        Array.Resize(ref this.values, this.values.Length - 1);
        return value;
    }
}

Dieser Code ist nicht sehr wiederverwendbar. Wenn später Stacks für Werte anderer Datentypen wie String, Dezimal usw. benötigt werden, gibt es einige Optionen:

  • Erstellen Sie für jeden neuen Datentyp eine Kopie des obigen Codes und ändern Sie die int-Typinformationen. Also ISTringStack und StringStack kann für string definiert werden , IDecimalStack und Dezimalstapel für dezimal , und so weiter und weiter. Anscheinend ist dieser Weg nicht machbar.
  • Da jeder Typ von object abgeleitet ist , ein allgemeiner Stack für object definiert werden, das ist IObjectStack und ObjectStack . Der Push Methode akzeptiert Objekt und Pop Methode gibt Objekt zurück , sodass der Stack für Werte beliebigen Datentyps verwendet werden kann. Dieses Design verliert jedoch die Überprüfung des Kompilierungszeittyps. Push aufrufen mit beliebigen Argumenten kompiliert werden. Auch zur Laufzeit, wenn Pop aufgerufen wird, muss das zurückgegebene Objekt in den erwarteten Typ gecastet werden, was einen Performance-Overhead und ein Fehlerrisiko darstellt.

Parameter eingeben

Bei Generika ist es viel besser, den konkreten Typ int durch einen Typparameter T zu ersetzen, der in spitzen Klammern nach dem Stack-Typnamen deklariert wird:

internal interface IStack<T>
{
    void Push(T value);

    T Pop();
}

internal class Stack<T> : IStack<T>
{
    private T[] values = new T[0];

    public void Push(T value)
    {
        Array.Resize(ref this.values, this.values.Length + 1);
        this.values[this.values.Length - 1] = value;
    }

    public T Pop()
    {
        if (this.values.Length == 0)
        {
            throw new InvalidOperationException("Stack empty.");
        }
        T value = this.values[this.values.Length - 1];
        Array.Resize(ref this.values, this.values.Length - 1);
        return value;
    }
}

Geben Sie bei Verwendung dieses generischen Stacks einen konkreten Typ für den Parameter T:

an
internal static void Stack()
{
    Stack<int> stack1 = new Stack<int>();
    stack1.Push(int.MaxValue);
    int value1 = stack1.Pop();

    Stack<string> stack2 = new Stack<string>();
    stack2.Push(Environment.MachineName);
    string value2 = stack2.Pop();

    Stack<Uri> stack3 = new Stack<Uri>();
    stack3.Push(new Uri("https://weblogs.asp.net/dixin"));
    Uri value3 = stack3.Pop();
}

Generika ermöglichen also die Wiederverwendung von Code mit Typsicherheit. IStack und Stack sind stark typisiert, wobei IStack. Drücken /Stack.Push akzeptieren Sie einen Wert vom Typ T und IStack Pop /IStack.Pop gibt einen Wert vom Typ T zurück . Beispiel:Wenn T ist int , IStack .Drücken /Stack.Push akzeptiere einen int Wert; Wenn T ist Zeichenfolge , IStack.Pop /Stack.Pop gibt einen String zurück Wert; usw. Also IStack und Stack sind polymorphe Typen, und dies wird parametrischer Polymorphismus genannt.

In .NET wird ein generischer Typ mit Typparametern als offener Typ (oder offen konstruierter Typ) bezeichnet. Wenn alle Typparameter des generischen Typs mit konkreten Typen angegeben werden, spricht man von geschlossenem Typ (oder geschlossen konstruiertem Typ). Hier Stack ist offener Typ und Stack , Stack , Stack sind geschlossene Typen.

Die Syntax für die generische Struktur ist dieselbe wie oben für die generische Klasse. Generische Delegaten und generische Methoden werden später besprochen.

Typparametereinschränkungen

Für die obigen generischen Typen und die folgenden generischen Typen kann der Typparameter ein beliebiger Wert sein:

internal class Constraint<T>
{
    internal void Method()
    {
        T value = null;
    }
}

Der obige Code kann nicht kompiliert werden, mit Fehler CS0403:Null kann nicht in den Typparameter „T“ konvertiert werden, da es sich um einen Werttyp handeln könnte, der keine Nullwerte zulässt. Der Grund ist, wie oben erwähnt, dass nur Werte von Referenztypen (Instanzen von Klassen) null sein können , aber hier T darf auch Strukturtyp sein. Für diese Art von Szenario unterstützt C# Einschränkungen für Typparameter mit dem Schlüsselwort where:

internal class Constraint<T> where T: class
{
    internal static void Method()
    {
        T value1 = null;
    }
}

Hier muss T ein Referenztyp sein, zum Beispiel Constraint wird vom Compiler zugelassen und Constraint verursacht einen Compilerfehler. Hier sind einige weitere Beispiele für die Einschränkungssyntax:

internal partial class Constraints<T1, T2, T3, T4, T5, T6, T7>
    where T1 : struct
    where T2 : class
    where T3 : DbConnection
    where T4 : IDisposable
    where T5 : struct, IComparable, IComparable<T5>
    where T6 : new()
    where T7 : T2, T3, T4, IDisposable, new() { }

Der obige generische Typ hat 7 Typparameter:

  • T1 muss Werttyp (Struktur) sein
  • T2 muss Referenztyp (Klasse) sein
  • T3 muss der angegebene Typ sein oder vom angegebenen Typ abgeleitet sein
  • T4 muss die angegebene Schnittstelle sein oder die angegebene Schnittstelle implementieren
  • T5 muss vom Werttyp (Struktur) sein und alle angegebenen Schnittstellen implementieren
  • T6 muss einen öffentlichen parameterlosen Konstruktor haben
  • T7 muss T2 sein oder von ihm abgeleitet sein oder es implementieren , T3 , T4 und muss die angegebene Schnittstelle implementieren und einen öffentlichen parameterlosen Konstruktor haben

Nehmen Sie T3 als Beispiel:

internal partial class Constraints<T1, T2, T3, T4, T5, T6, T7>
{
    internal static void Method(T3 connection)
    {
        using (connection) // DbConnection implements IDisposable.
        {
            connection.Open(); // DbConnection has Open method.
        }
    }
}

Bezüglich System.Data.Common.DbConnection implementiert System.IDisposable , und hat einen CreateCommand Methode, sodass das obige t3-Objekt mit der using-Anweisung und dem CreateCommand verwendet werden kann Aufruf kann auch kompiliert werden.

Das Folgende ist ein Beispiel für einen geschlossenen Typ von Constraints :

internal static void CloseType()
{
    Constraints<bool, object, DbConnection, IDbConnection, int, Exception, SqlConnection> closed = default;
}

Hier:

  • bool ist Werttyp
  • Objekt ist Referenztyp
  • DbConnection ist DbConnection
  • System.Data.Common.IDbConnection implementiert IDisposable
  • int ist ein Werttyp, implementiert System.IComparable und implementiert auch System.IComparable
  • System.Exception hat einen öffentlichen parameterlosen Konstruktor
  • System.Data.SqlClient.SqlConnection leitet sich von Objekt ab, leitet sich von DbConnection ab, implementiert IDbConnection und hat einen öffentlichen parameterlosen Konstruktor

Nullable-Werttyp

Wie bereits erwähnt, darf in C#/.NET die Instanz des Typs nicht null sein. Es gibt jedoch noch einige Szenarien, in denen der Werttyp logisch null darstellt. Ein typisches Beispiel ist eine Datenbanktabelle. Ein aus einer Nullable-Integer-Spalte abgerufener Wert kann entweder ein Integer-Wert oder null sein. C# 2.0 führt eine Nullable-Werttyp-Syntax T? ein, zum Beispiel int? liest nullable int. T? ist nur eine Abkürzung der generischen System.Nullable-Struktur:

namespace System
{
    public struct Nullable<T> where T : struct
    {
        private bool hasValue;

        internal T value;

        public Nullable(T value)
        {
            this.value = value;
            this.hasValue = true;
        }

        public bool HasValue
        {
            get { return this.hasValue; }
        }

        public T Value
        {
            get
            {
                if (!this.hasValue)
                {
                    throw new InvalidOperationException("Nullable object must have a value.");
                }
                return this.value;
            }
        }

        // Other members.
    }
}

Das folgende Beispiel demonstriert die Verwendung von nullable int:

internal static void Nullable()
{
    int? nullable = null;
    nullable = 1;
    if (nullable != null)
    {
        int value = (int)nullable;
    }
}

Anscheinend, int? ist die Nullable-Struktur und kann nicht wirklich null sein. Der obige Code ist syntaktischer Zucker und nach normaler Strukturverwendung kompiliert:

internal static void CompiledNullable()
{
    Nullable<int> nullable = new Nullable<int>();
    nullable = new Nullable<int>(1);
    if (nullable.HasValue)
    {
        int value = nullable.Value;
    }
}

Wenn nullable mit null zugewiesen wird, wird es tatsächlich mit einer Instanz von Nullable instance zugewiesen. Hier wird der parameterlose Standardkonstruktor der Struktur aufgerufen, sodass eine Nullable-Instanz initialisiert wird, wobei jedes Datenfeld mit seinem Standardwert initialisiert wird. Das hasValue-Feld von nullable ist also falsch, was darauf hinweist, dass diese Instanz logisch null darstellt. Dann wird nullable mit dem normalen int-Wert neu zugewiesen, es wird tatsächlich einer anderen Nullable-Instanz zugewiesen, wobei das hasValue-Feld auf true und das value-Feld auf den angegebenen int-Wert festgelegt wird. Die Nicht-Null-Prüfung wird zum HasValue-Eigenschaftsaufruf kompiliert. Und die Typkonvertierung von int? to int wird zum Aufruf der Value-Eigenschaft kompiliert.

Automatische Eigenschaft

Eine Eigenschaft ist im Wesentlichen ein Getter mit Body und/oder ein Setter mit Body. In vielen Fällen umschließt der Setter und Getter einer Eigenschaft nur ein Datenfeld, wie die Name-Eigenschaft des obigen Gerätetyps. Dieses Muster kann störend sein, wenn ein Typ viele Eigenschaften zum Umschließen von Datenfeldern hat, daher führt C# 3.0 die syntaktische Eigenschaft auto ein:

internal partial class Device
{
    internal decimal Price { get; set; }
}

Die Hintergrundfelddefinition und der Körper von Getter/Setter werden vom Compiler generiert:

internal class CompiledDevice
{
    [CompilerGenerated]
    private decimal priceBackingField;

    internal decimal Price
    {
        [CompilerGenerated]
        get { return this.priceBackingField; }

        [CompilerGenerated]
        set { this.priceBackingField = value; }
    }

    // Other members.
}

Seit C# 6.0 kann die Eigenschaft auto nur getter sein:

internal partial class Category
{
    internal Category(string name)
    {
        this.Name = name;
    }

    internal string Name { get; }
}

Die obige Name-Eigenschaft wird so kompiliert, dass sie nur Getter enthält, und das Hintergrundfeld wird schreibgeschützt:

internal partial class CompiledCategory
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string nameBackingField;

    internal CompiledCategory(string name)
    {
        this.nameBackingField = name;
    }

    internal string Name
    {
        [CompilerGenerated]
        get { return this.nameBackingField; }
    }
}

Eigenschaftsinitialisierer

C# 6.0 führt den syntaktischen Zucker des Eigenschaftsinitialisierers ein, sodass der Anfangswert der Eigenschaft inline bereitgestellt werden kann:

internal partial class Category
{
    internal Guid Id { get; } = Guid.NewGuid();

    internal string Description { get; set; } = string.Empty;
}

Der Eigenschaftsinitialisierer wird zum Unterstützungsfeldinitialisierer kompiliert:

internal partial class CompiledCategory
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly Guid idBackingField = Guid.NewGuid();

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string descriptionBackingField = string.Empty;

    internal Guid Id
    {
        [CompilerGenerated]
        get { return this.idBackingField; }
    }

    internal string Description
    {
        [CompilerGenerated]
        get { return this.descriptionBackingField; }

        [CompilerGenerated]
        set { this.descriptionBackingField = value; }
    }
}

Objektinitialisierer

Eine Geräteinstanz kann mit einer Folge zwingender Eigenschaftszuweisungsanweisungen initialisiert werden:

internal static void SetProperties()
{
    Device device = new Device();
    device.Name = "Surface Book";
    device.Price = 1349M;
}

C# 3.0 führt den Objektinitialisierer syntaktischen Zucker ein, der obige Aufrufkonstruktor und der Code zum Festlegen von Eigenschaften können in einem deklarativen Stil zusammengeführt werden:

internal static void ObjectInitializer()
{
    Device device = new Device() { Name = "Surface Book", Price = 1349M };
}

Die Syntax des Objektinitialisierers im zweiten Beispiel wird im ersten Beispiel zu einer Folge von Zuweisungen kompiliert.

Sammlungsinitialisierer

In ähnlicher Weise führt C# 3.0 auch den syntaktischen Sugar der Auflistungsinitialisierer für den Typ ein, der die System.Collections.IEnumerable-Schnittstelle implementiert und über eine parametrisierte Add-Methode verfügt. Nehmen Sie die folgende Gerätesammlung als Beispiel:

internal class DeviceCollection : IEnumerable
{
    private Device[] devices = new Device[0];

    internal void Add(Device device)
    {
        Array.Resize(ref this.devices, this.devices.Length + 1);
        this.devices[this.devices.Length - 1] = device;
    }

    public IEnumerator GetEnumerator() // From IEnumerable.
    {
        return this.devices.GetEnumerator();
    }
}

Es kann auch deklarativ initialisiert werden:

internal static void CollectionInitializer(Device device1, Device device2)
{
    DeviceCollection devices = new DeviceCollection() { device1, device2 };
}

Der obige Code wird zu einem normalen Konstruktoraufruf kompiliert, gefolgt von einer Folge von Add-Methodenaufrufen:

internal static void CompiledCollectionInitializer(Device device1, Device device2)
{
    DeviceCollection devices = new DeviceCollection();
    devices.Add(device1);
    devices.Add(device2);
}

Index-Initialisierer

C# 6.0 führt Index-Initialisierer für Typ mit Indexer-Setter ein:

internal class DeviceDictionary
{
    internal Device this[int id] { set { } }
}

Es ist ein weiterer deklarativer syntaktischer Zucker:

internal static void IndexInitializer(Device device1, Device device2)
{
    DeviceDictionary devices = new DeviceDictionary { [10] = device1, [11] = device2 };
}

Die obige Syntax wird zu einem normalen Konstruktoraufruf kompiliert, gefolgt von einer Folge von Indexeraufrufen:

internal static void CompiledIndexInitializer(Device device1, Device device2)
{
    DeviceDictionary devices = new DeviceDictionary();
    devices[0] = device1;
    devices[1] = device2;
}

Null-Koaleszenzoperator

C# 2.0 führt einen Null-Coalescing-Operator ?? ein. Es funktioniert mit 2 Operanden als links ?? Rechts. Wenn der linke Operand nicht null ist, wird der linke Operand zurückgegeben, andernfalls der rechte Operand. Wenn Sie beispielsweise mit Referenz- oder Nullable-Werten arbeiten, ist es sehr üblich, zur Laufzeit auf null zu prüfen und null zu ersetzen:

internal partial class Point
{
    internal static Point Default { get; } = new Point(0, 0);
}

internal partial struct ValuePoint
{
    internal static ValuePoint Default { get; } = new ValuePoint(0, 0);
}

internal static void DefaultValueForNull(Point reference, ValuePoint? nullableValue)
{
    Point point = reference != null ? reference : Point.Default;

    ValuePoint valuePoint = nullableValue != null ? (ValuePoint)nullableValue : ValuePoint.Default;
}

Dies kann mit dem Null-Coalescing-Operator vereinfacht werden:

internal static void DefaultValueForNullWithNullCoalescing(Point reference, ValuePoint? nullableValue)
{
    Point point = reference ?? Point.Default;
    ValuePoint valuePoint = nullableValue ?? ValuePoint.Default;
}

Null bedingte Operatoren

Es ist auch sehr üblich, null vor dem Member- oder Indexer-Zugriff zu prüfen:

internal static void NullCheck(Category category, Device[] devices)
{
    string categoryText = null;
    if (category != null)
    {
        categoryText = category.ToString();
    }
    string firstDeviceName;
    if (devices != null)
    {
        Device firstDevice = devices[0];
        if (first != null)
        {
            firstDeviceName = firstDevice.Name;
        }
    }
}

C# 6.0 führt bedingte Nulloperatoren (auch als Nullweitergabeoperatoren bezeichnet) ein, ?. für Member-Zugriff und ?[] für Indexer-Zugriff, um dies zu vereinfachen:

internal static void NullCheckWithNullConditional(Category category, Device[] devices)
{
    string categoryText = category?.ToString();
    string firstDeviceName = devices?[0]?.Name;
}

Wurfausdruck

Seit C# 7.0 kann die Throw-Anweisung als Ausdruck verwendet werden. Der Throw-Ausdruck wird häufig mit dem Bedingungsoperator und dem Koaleszenzoperator über Null verwendet, um die Argumentüberprüfung zu vereinfachen:

internal partial class Subcategory
{
    internal Subcategory(string name, Category category)
    {
        this.Name = !string.IsNullOrWhiteSpace(name) ? name : throw new ArgumentNullException("name");
        this.Category = category ?? throw new ArgumentNullException("category");
    }

    internal Category Category { get; }

    internal string Name { get; }
}

Ausnahmefilter

In C# war es früher üblich, eine Ausnahme abzufangen, zu filtern und dann zu behandeln/neu auszulösen. Im folgenden Beispiel wird versucht, eine HTML-Zeichenfolge vom angegebenen URI herunterzuladen, und es kann den Downloadfehler behandeln, wenn der Antwortstatus „Bad Request“ lautet. Es fängt also die zu überprüfende Ausnahme ab. Wenn die Ausnahme Informationen erwartet hat, behandelt sie die Ausnahme; Andernfalls wird die Ausnahme erneut ausgelöst.

internal static void CatchFilterRethrow(WebClient webClient)
{
    try
    {
        string html = webClient.DownloadString("http://weblogs.asp.net/dixin");
    }
    catch (WebException exception)
    {
        if ((exception.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.BadRequest)
        {
            // Handle exception.
        }
        else
        {
            throw;
        }
    }
}

C# 6.0 führt Ausnahmefilter auf Sprachebene ein. Der catch-Block kann einen Ausdruck haben, um die angegebene Ausnahme vor dem Fangen zu filtern. Wenn der Ausdruck wahr zurückgibt, wird der Catch-Block ausgeführt:

internal static void ExceptionFilter(WebClient webClient)
{
    try
    {
        string html = webClient.DownloadString("http://weblogs.asp.net/dixin");
    }
    catch (WebException exception) when ((exception.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.BadRequest)
    {
        // Handle exception.
    }
}

Der Ausnahmefilter ist kein syntaktischer Zucker, sondern ein CLR-Feature. Der obige Ausnahmefilterausdruck wird zur Filterklausel in CIL kompiliert. Die folgende bereinigte CIL demonstriert virtuell das Kompilierungsergebnis:

.method assembly hidebysig static void ExceptionFilter(class [System]System.Net.WebClient webClient) cil managed
{
  .try
  {
    // string html = webClient.DownloadString("http://weblogs.asp.net/dixin");
  }
  filter
  {
    // when ((exception.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.BadRequest)
  }
  {
    // Handle exception.
  }
}

Wenn der Filterausdruck „false“ zurückgibt, wird die catch-Klausel nie ausgeführt, sodass die Ausnahme nicht erneut ausgelöst werden muss. Das erneute Auslösen einer Ausnahme führt zum Entladen des Stapels, als ob die Ausnahme aus der throw-Anweisung stammt, und der ursprüngliche Aufrufstapel und andere Informationen gehen verloren. Daher ist diese Funktion sehr hilfreich für die Diagnose und das Debugging.

String-Interpolation

Seit vielen Jahren wird die zusammengesetzte Formatierung von Zeichenfolgen in C# häufig verwendet. Es fügt Werte in indizierte Platzhalter im String-Format ein:

internal static void Log(Device device)
{
    string message = string.Format("{0}: {1}, {2}", DateTime.Now.ToString("o"), device.Name, device.Price);
    Trace.WriteLine(message);
}

C# 6.0 führt die syntaktische Zeichenfolgeninterpolation ein, um die Werte an Ort und Stelle zu deklarieren, ohne die Reihenfolgen separat zu verwalten:

internal static void LogWithStringInterpolation(Device device)
{
    string message = string.Format($"{DateTime.Now.ToString("o")}: {device.Name}, {device.Price}");
    Trace.WriteLine(message);
}

Die zweite Interpolationsversion ist deklarativer und produktiver, ohne eine Reihe von Indizes zu pflegen. Diese Syntax wird tatsächlich in die erste zusammengesetzte Formatierung kompiliert.

Name des Betreibers

C# 6.0 führt einen nameof-Operator ein, um den Zeichenfolgennamen einer Variablen, eines Typs oder eines Members abzurufen. Nehmen wir als Beispiel die Argumentüberprüfung:

internal static void ArgumentCheck(int count)
{
    if (count < 0)
    {
        throw new ArgumentOutOfRangeException("count");
    }
}

Der Parametername ist eine fest codierte Zeichenfolge und kann vom Compiler nicht überprüft werden. Jetzt kann der Compiler mit nameof-Operator die obige Parameternamen-String-Konstante generieren:

internal static void NameOf(int count)
{
    if (count < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(count));
    }
}

Zifferntrenner und führender Unterstrich

C# 7.0 führt den Unterstrich als Zifferntrennzeichen sowie das Präfix 0b für Binärzahlen ein. C# 7.1 unterstützt einen optionalen Unterstrich am Anfang der Zahl.

internal static void DigitSeparator()
{
    int value1 = 10_000_000;
    double value2 = 0.123_456_789;

    int value3 = 0b0001_0000; // Binary.
    int value4 = 0b_0000_1000; // Binary.
}

Diese kleinen Funktionen verbessern die Lesbarkeit von langen Zahlen und Binärzahlen zur Entwurfszeit erheblich.

Zusammenfassung

Dieses Kapitel führt Sie durch grundlegendes und wichtiges Wissen über C#, wie Referenztyp, Werttyp, generischer Typ, Nullable-Werttyp und einige grundlegende Syntax von Initialisierern, Operatoren, Ausdrücken usw., einschließlich einiger neuer Syntaxen, die in den letzten Versionen von C# eingeführt wurden. Nachdem Sie sich mit diesen Grundlagen vertraut gemacht haben, sollten die Leser bereit sein, in andere fortgeschrittene Themen der C#-Sprache, funktionale Programmierung und LINQ einzutauchen.