Approfondimenti sulla programmazione funzionale C# (1) Nozioni di base sul linguaggio C#

Approfondimenti sulla programmazione funzionale C# (1) Nozioni di base sul linguaggio C#

[LINQ tramite serie C#]

[Serie di approfondimento programmazione funzionale C#]

Ultima versione: https://weblogs.asp.net/dixin/functional-csharp-fundamentals

C# 1.0 è stato inizialmente rilasciato nel 2002, come dice la sua prima specifica del linguaggio all'inizio, C# è un linguaggio di programmazione "semplice, moderno, orientato agli oggetti e indipendente dai tipi" per uso generale. Ora C# si è evoluto a 7.2. Nel corso degli anni, a C# sono state aggiunte molte straordinarie funzionalità del linguaggio, in particolare ricche funzionalità di programmazione funzionale. Ora il linguaggio C# è stato produttivo ed elegante, imperativo e dichiarativo, orientato agli oggetti e funzionale. Con framework come .NET Framework, .NET Core, Mono, Xamarin, Unity, ecc., C# viene utilizzato da milioni di persone su piattaforme diverse, inclusi Windows, Linux, Mac, iOS, Android, ecc.

Questo tutorial è totalmente per il linguaggio C# e si concentra sui suoi aspetti funzionali. Si presume che i lettori abbiano i concetti generali sulla programmazione e sul linguaggio C#. Questo capitolo esamina gli elementi e la sintassi di base ma importanti di C# 1.0 - 7.x, per riscaldare i lettori di livello principiante, così come i lettori che non hanno ancora familiarità con alcune nuove sintassi introdotte nelle recenti versioni di C#. Le altre caratteristiche e concetti avanzati saranno discussi in dettaglio nei capitoli successivi. Questo tutorial non copre gli argomenti e le caratteristiche del linguaggio al di fuori dell'ambito della programmazione funzionale e LINQ, come l'ereditarietà della programmazione orientata agli oggetti, il puntatore in codice non sicuro, l'interoperabilità con altro codice non gestito, la programmazione dinamica, ecc.

C# Caratteristiche in questo capitolo Caratteristiche in altri capitoli Funzioni non coperte
1.0 Classe
Struttura
Interfaccia
Enumerazione
usando la dichiarazione
Delega
Evento
Membro di funzione
parametro di riferimento
fuori parametro
Matrice di parametri
foreach
Ereditarietà
Puntatore
Interoperabilità
1.1 direttiva pragma
1.2 foreach per IDisposable
2.0 Classe statica
Tipo parziale
Tipo generico
Tipo di valore nullable
Operatore di coalescenza nullo
Metodo anonimo
Generatore
Covarianza e controvarianza
Metodo generico
3.0 Proprietà automatica
Inizializzatore di oggetti
Inizializzatore di raccolta
Tipo anonimo
Variabile locale digitata implicitamente
Espressione di query
Espressione Lambda
Metodo di estensione
Metodo parziale
4.0 Argomento denominato
Argomento facoltativo
Covarianza e controvarianza generica
Legatura dinamica
5.0 Funzione asincrona
Argomento delle informazioni sul chiamante
6.0 Inizializzatore di proprietà
Inizializzatore di dizionario
Operatore di propagazione nullo
Filtro eccezione
Interpolazione di stringhe
nomedell'operatore
Importazione statica
Membro con corpo di espressione
wait in catch/finally block
7.0 genera espressione
Separatore di cifre
Variabile in uscita
Tupla e decostruzione
Funzione locale
Membro con corpo di espressione espansa
rif ritorno e locale
Scartare
Rendimento asincrono generalizzato
lanciare l'espressione
Corrispondenza del modello
7.1 espressione letterale predefinita Metodo principale asincrono
Nome elemento tupla dedotto
7.2 struttura di riferimento
Caratteri di sottolineatura iniziali in valori letterali numerici
Argomenti denominati non finali
nel parametro
ref readonly ritorno e locale
Struttura di sola lettura
modificatore privato protetto

Tipi e membri

C# è fortemente tipizzato. In C#, qualsiasi valore ha un tipo. C# supporta 5 tipi di tipi:classe, struttura, enumerazione, delegato e interfaccia.

Una classe è un tipo di riferimento definito con la parola chiave class. Può avere campi, proprietà, metodi, eventi, operatori, indicizzatori, costruttori, distruttori e tipi nidificati di classe, struttura, enumerazione, delegato e interfaccia. Una classe è sempre derivata da System.Object classe.

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.
    }
}

L'oggetto ha un metodo Equals statico per verificare se 2 istanze sono considerate uguali, un metodo Equals di istanza per verificare se l'istanza corrente e l'altra istanza sono considerate uguali e un metodo ReferenceEquals statico per verificare se 2 istanze sono la stessa istanza. Ha un metodo GetHashCode come funzione hash predefinita per restituire un numero di codice hash per un rapido test delle istanze. Ha anche un metodo GetType per restituire il tipo dell'istanza corrente e un metodo ToString per restituire la rappresentazione testuale dell'istanza corrente.

L'esempio seguente è un segmento dell'implementazione della classe System.Exception in .NET Framework. Dimostra la sintassi per definire una classe e diversi tipi di membri. Questa classe implementa l'interfaccia System.ISerializable e deriva la classe System._Exception. Quando si definisce una classe, la classe base System.Object può essere omessa.

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.
    }
}

Una struttura è un tipo di valore definito con la parola chiave struct, che viene quindi derivata da System.Object classe. Può avere tutti i tipi di membri della classe tranne il distruttore. Una struttura deriva sempre da System.ValueType class e, cosa interessante, System.ValueType è un tipo di riferimento derivato da System.Object. In pratica, una struttura è solitamente definita per rappresentare una struttura di dati molto piccola e immutabile, al fine di migliorare le prestazioni di allocazione/disallocazione della memoria. Ad esempio, il . Nel sistema .NET Core. è implementato come:

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.
    }
}

Un'enumerazione è un tipo di valore derivato dalla classe System.Enum, che è derivata dalla classe System.ValueType. Può avere solo campi costanti del tipo integrale sottostante specificato (int per impostazione predefinita). Ad esempio:

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

Un delegato è un tipo di riferimento derivato da System.MulticastDelegate class, che deriva da System.Delegate classe. Il tipo di delegato rappresenta il tipo di funzione ed è discusso in dettaglio nel capitolo sulla programmazione funzionale.

namespace System
{
    public delegate void Action();
}

Un'interfaccia è un contratto da implementare per classe o struttura. L'interfaccia può avere solo proprietà, metodi ed eventi pubblici e astratti senza implementazione. Ad esempio:

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

        bool HasErrors { get; } // Property.

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

Qualsiasi classe o struttura che implementa l'interfaccia di cui sopra deve avere i 3 membri specificati come pubblici.

Tipi integrati

Ci sono di base. NET più comunemente usati nella programmazione C#, quindi C# fornisce parole chiave del linguaggio come alias di quei tipi, che sono chiamati tipi incorporati di C#:

Parola chiave C# Tipo .NET
bool System.Boolean
sbyte System.SByte
byte Byte di sistema
char System.Char
corto System.Init16
ushort System.UInit16
int System.Init32
uint System.UInit32
lungo System.Init54
ulong System.UInit54
flottante System.Single
doppio System.Double
decimale System.Decimal
oggetto System.Object
stringa System.String

Tipo di riferimento e tipo di valore

In C#/.NET, le classi sono tipi di riferimento, inclusi oggetti, stringhe, array e così via. Anche i delegati sono tipi di riferimento, di cui si parlerà più avanti. Le strutture sono tipi di valore, inclusi i tipi primitivi (bool , sbyte , byte , carattere , breve , ubreve , int , uint , lungo , lungo , fluttuante , doppio ), decimale , System.DateTime , System.DateTimeOffset , System.TimeSpan , Guida.Sistema , Sistema.Nullable , enumerazione (poiché il tipo sottostante dell'enumerazione è sempre un tipo primitivo numerico), ecc. L'esempio seguente definisce un tipo di riferimento e un tipo di valore, che sembrano simili tra loro:

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; } }
}

Le istanze del tipo di riferimento e del tipo di valore vengono allocate in modo diverso. Il tipo di riferimento viene sempre allocato nell'heap gestito e deallocato tramite Garbage Collection. Il tipo di valore viene allocato nello stack e deallocato tramite la rimozione dello stack oppure viene allocato e deallocato in linea con il contenitore. Quindi generalmente il tipo di valore può avere prestazioni migliori per l'allocazione e la deallocazione. Di solito, un tipo può essere progettato come tipo valore se è piccolo, immutabile e logicamente simile a un tipo primitivo. Quanto sopra System.TimeSpan la struttura del tipo rappresenta una durata di tempo, è progettata per essere un tipo di valore, perché è solo un wrapper immutabile di un valore lungo, che rappresenta i tick. L'esempio seguente mostra questa differenza:

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) };
    }
}

Quando un Punto istanza viene costruita come variabile locale, poiché è di tipo riferimento, viene allocata nell'heap gestito. I suoi campi sono tipi di valore, quindi i campi vengono allocati in linea anche nell'heap gestito. La variabile locale reference1 può essere visualizzato come un puntatore, che punta alla posizione dell'heap gestita che contiene i dati. Durante l'assegnazione di riferimento1 a riferimento2 , il puntatore viene copiato. Quindi riferimento1 e riferimento2 entrambi puntano allo stesso Punto istanza nell'heap gestito. Quando ValuePoint è costruito come una variabile locale, poiché è di tipo valore. è allocato nello stack. I suoi campi sono anche allocati in linea nello stack. La variabile locale value1 contiene i dati effettivi. Quando si assegna valore1 a valore2 , l'intera istanza viene copiata, quindi value1 e valore2 sono 2 diversi ValuePoint istanze in pila. In C#, array deriva sempre dalla classe System.Array ed è un tipo di riferimento. Quindi referenceArray e valueArray sono entrambi allocati nell'heap e anche i loro elementi sono entrambi nell'heap.

Il tipo di riferimento può essere null e il tipo di valore no:

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
}

Il valore predefinito del tipo di riferimento è semplicemente null. L'impostazione predefinita del tipo di valore è un'istanza effettiva, con tutti i campi inizializzati sui valori predefiniti. In realtà, l'inizializzazione delle variabili locali di cui sopra viene compilata in:

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

    ValuePoint defaultValue = new ValuePoint();
}

Una struttura ha sempre virtualmente un costruttore predefinito senza parametri. La chiamata a questo costruttore predefinito crea un'istanza della struttura e imposta tutti i suoi campi sui valori predefiniti. Qui valore predefinito è int i campi vengono inizializzati su 0. Se ValuePoint dispone di un campo del tipo di riferimento, il campo del tipo di riferimento viene inizializzato su null.

espressione letterale predefinita

Da C# 7.1, il tipo nell'espressione del valore predefinita può essere omesso, se il tipo può essere dedotto. Quindi la sintassi del valore predefinito sopra può essere semplificata in:

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

    ValuePoint defaultValue = default;
}

rif struttura

C# 7.2 abilita la parola chiave ref per la definizione della struttura, in modo che la struttura possa essere allocata solo nello stack. Questo può essere utile per scenari critici per le prestazioni, in cui l'allocazione/distribuzione della memoria nell'heap può causare un sovraccarico delle prestazioni.

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.
}

Come accennato in precedenza, array è un tipo di riferimento allocato nell'heap, quindi il compilatore non consente un array di struttura ref. Un'istanza di classe viene sempre istanziata nell'heap, quindi la struttura ref non può essere utilizzata come campo. Un'istanza di struttura normale può essere in pila o in heap, quindi neanche la struttura di riferimento può essere utilizzata come campo.

Classe statica

C# 2.0 abilita statico modificatore per la definizione della classe. Prendi System.Math come esempio:

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

Una classe statica può avere solo membri statici e non può essere istanziata. La classe statica viene compilata in una classe sigillata astratta. In C# static viene spesso utilizzato per ospitare una serie di metodi statici.

Tipo parziale

C# 2.0 introduce il parziale parola chiave per dividere la definizione di classe, struttura o interfaccia in fase di progettazione.

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(); }
    }
}

Questo è utile per gestire caratteri di grandi dimensioni suddividendoli in più file più piccoli. Anche i tipi parziali vengono utilizzati frequentemente nella generazione del codice, in modo che l'utente possa aggiungere codice personalizzato ai tipi generati dallo strumento. In fase di compilazione, le parti multiple di un tipo vengono unite.

Interfaccia e implementazione

Quando un tipo implementa un'interfaccia, questo tipo può implementare ogni membro dell'interfaccia in modo implicito o esplicito. L'interfaccia seguente ha 2 metodi membri:

internal interface IInterface
{
    void Implicit();

    void Explicit();
}

E il seguente tipo che implementa questa interfaccia:

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

    void IInterface.Explicit() { }
}

Queste Implementazioni tipo ha un Implicito pubblico metodo con la stessa firma di IInterface è implicito metodo, quindi il compilatore C# accetta implementazioni. Metodo implicito come implementazione di IInterface. Metodo implicito. Questa sintassi è chiamata implementazione dell'interfaccia implicita. L'altro metodo Explicit viene implementato in modo esplicito come membro dell'interfaccia, non come metodo membro di tipo Implementazioni. L'esempio seguente mostra come utilizzare questi membri dell'interfaccia:

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

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

È possibile accedere a un membro dell'interfaccia implementato in modo implicito dall'istanza del tipo di implementazione e dal tipo di interfaccia, ma è possibile accedere a un membro dell'interfaccia implementato in modo esplicito solo dall'istanza del tipo di interfaccia. Qui il nome della variabile @oggetto e @interfaccia sono preceduti dal carattere speciale @, perché oggetto e interfaccia sono parole chiave del linguaggio C# e non possono essere utilizzate direttamente come identificatore.

Interfaccia IDiposable e istruzione using

In fase di esecuzione, CLR/CoreCLR gestisce automaticamente la memoria. Alloca memoria per oggetti .NET e rilascia la memoria con Garbage Collector. Un oggetto .NET può anche allocare altre risorse non gestite da CLR/CoreCLR, come file aperti, handle di finestra, connessioni a database, ecc. .NET fornisce un contratto standard per questi tipi:

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

Un tipo che implementa l'interfaccia System.IDiposable sopra deve avere un metodo Dispose, che rilascia in modo esplicito le sue risorse non gestite quando viene chiamato. Ad esempio, System.Data.SqlClient.SqlConnection rappresenta una connessione a un database SQL, implementa IDisposable e fornisce il metodo Dispose per rilasciare la connessione al database sottostante. L'esempio seguente è il modello try-finally standard per utilizzare l'oggetto IDisposable e chiamare il metodo Dispose:

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();
        }
    }
}

Il metodo Dispose viene chiamato in finally block, quindi è garantito che venga chiamato, anche se viene generata un'eccezione dalle operazioni nel blocco try o se il thread corrente viene interrotto. IDisposable è ampiamente utilizzato, quindi C# introduce uno zucchero sintattico dell'istruzione using dalla 1.0. Il codice sopra è equivalente a:

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

Questo è più dichiarativo in fase di progettazione e try-finally viene generato in fase di compilazione. Le istanze usa e getta devono essere sempre utilizzate con questa sintassi, per garantire che il metodo Dispose venga chiamato nel modo corretto.

Tipo generico

C# 2.0 introduce la programmazione generica. La programmazione generica è un paradigma che supporta i parametri di tipo, in modo che le informazioni sul tipo possano essere fornite in un secondo momento. La seguente struttura di dati dello stack di int i valori non sono generici:

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;
    }
}

Questo codice non è molto riutilizzabile. Successivamente, se sono necessari stack per valori di altri tipi di dati, come stringa, decimale e così via, sono disponibili alcune opzioni:

  • Per ogni nuovo tipo di dati, fai una copia del codice sopra e modifica le informazioni sul tipo int. Quindi IStringStack e StringStack può essere definito per stringa , IDecimalStack e DecimaleImpila per decimale , e così via. Apparentemente in questo modo non è fattibile.
  • Poiché ogni tipo è derivato da oggetto , uno stack generale per oggetto può essere definito, che è IObjectStack e ObjectStack . La Punta il metodo accetta oggetto e Pop il metodo restituisce oggetto , quindi lo stack può essere utilizzato per valori di qualsiasi tipo di dati. Tuttavia, questo progetto perde il controllo del tipo in fase di compilazione. Chiamando Push con qualsiasi argomento può essere compilato. Inoltre, in fase di esecuzione, ogni volta che Pop viene chiamato, è necessario eseguire il cast dell'oggetto restituito sul tipo previsto, che rappresenta un sovraccarico di prestazioni e una possibilità di errore.

Digitare parametro

Con i generics, un'opzione molto migliore consiste nel sostituire il tipo concrete int con un parametro di tipo T, dichiarato tra parentesi angolari dopo il nome del tipo di stack:

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;
    }
}

Quando si utilizza questo stack generico, specificare un tipo concreto per il parametro T:

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();
}

Quindi i generics consentono il riutilizzo del codice con la sicurezza del tipo. IStack e Impila sono di tipo forte, dove IStack. Spingi /Impila.Spingi accetta un valore di tipo T e IStack Pop /IStack.Pop restituire un valore di tipo T . Ad esempio, Quando T è int , Istack .Premi /Impila.Push accetta un int valore; Quando T è stringa , IStack.Pop /Impila.Pop restituisce una stringa valore; ecc. Quindi IStack e Impila sono tipi polimorfici, e questo è chiamato polimorfismo parametrico.

In .NET, un tipo generico con parametri di tipo viene chiamato tipo aperto (o tipo costruito aperto). Se tutti i parametri di tipo del tipo generico sono specificati con tipi concreti, viene chiamato tipo chiuso (o tipo costruito chiuso). Qui Impila è di tipo aperto e Stack , Impila , Impila sono tipi chiusi.

La sintassi per la struttura generica è la stessa della classe generica sopra. Il delegato generico e il metodo generico verranno discussi in seguito.

Digitare i vincoli dei parametri

Per i tipi generici precedenti e il tipo generico seguente, il parametro di tipo può essere un valore arbitrario:

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

Impossibile compilare il codice sopra, con errore CS0403:impossibile convertire null nel parametro di tipo 'T' perché potrebbe essere un tipo di valore non nullable. Il motivo è, come accennato, che solo i valori dei tipi di riferimento (istanze di classi) possono essere nulli , ma qui T è consentito anche essere di tipo struttura. Per questo tipo di scenario, C# supporta i vincoli per i parametri di tipo, con la parola chiave where:

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

Qui T deve essere un tipo di riferimento, ad esempio Vincolo è consentito dal compilatore e Vincolo provoca un errore del compilatore. Ecco alcuni altri esempi di sintassi dei vincoli:

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() { }

Il tipo generico sopra ha 7 parametri di tipo:

  • T1 deve essere di tipo valore (struttura)
  • T2 deve essere un tipo di riferimento (classe)
  • T3 deve essere del tipo specificato o derivare dal tipo specificato
  • T4 deve essere l'interfaccia specificata o implementare l'interfaccia specificata
  • T5 deve essere di tipo valore (struttura) e deve implementare tutte le interfacce specificate
  • T6 deve avere un costruttore pubblico senza parametri
  • T7 deve essere o derivare da o implementare T2 , T3 , T4 , e deve implementare l'interfaccia specificata e deve avere un costruttore pubblico senza parametri

Prendi T3 ad esempio:

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.
        }
    }
}

Riguardo a System.Data.Common.DbConnection implementa System.IDiposable e ha un CreateCommand metodo, quindi l'oggetto t3 sopra può essere utilizzato con l'istruzione using e il CreateCommand anche la chiamata può essere compilata.

Quello che segue è un esempio di tipo chiuso di Vincoli :

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

Qui:

  • bool è il tipo di valore
  • l'oggetto è un tipo di riferimento
  • DbConnection è DbConnection
  • System.Data.Common.IDbConnection implementa IDisposable
  • int è un tipo di valore, implementa System.IComparable e implementa anche System.IComparable
  • System.Exception ha un costruttore pubblico senza parametri
  • System.Data.SqlClient.SqlConnection deriva da oggetto, deriva da DbConnection, implementa IDbConnection e dispone di un costruttore pubblico senza parametri

Tipo di valore nullable

Come accennato in precedenza, in C#/.NET, l'istanza di tipo non può essere null. Tuttavia, esistono ancora alcuni scenari per il tipo di valore per rappresentare il null logico. Un tipico esempio è la tabella del database. Un valore recuperato da una colonna di numeri interi nullable può essere un valore intero o null. C# 2.0 introduce una sintassi di tipo valore nullable T?, ad esempio int? legge nullable int. T? è solo una scorciatoia della struttura generica System.Nullable:

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.
    }
}

L'esempio seguente mostra come usare nullable int:

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

A quanto pare, int? è la struttura Nullable e non può essere nullo reale. Il codice sopra è zucchero sintattico e compilato per il normale utilizzo della struttura:

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

Quando nullable viene assegnato con null, in realtà viene assegnato con un'istanza di Nullable instance. Qui viene chiamato il costruttore senza parametri predefinito della struttura, quindi viene inizializzata un'istanza Nullable, con ogni campo di dati viene inizializzato con il suo valore predefinito. Quindi il campo hasValue di nullable è false, indicando che questa istanza rappresenta logicamente null. Quindi nullable viene riassegnato con il valore int normale, in realtà viene assegnato con un'altra istanza Nullable, dove il campo hasValue è impostato su true e il campo value è impostato sul valore int specificato. Il controllo non nullo viene compilato nella chiamata della proprietà HasValue. E la conversione del tipo da int? to int viene compilato nella chiamata alla proprietà Value.

Proprietà auto

Una proprietà è essenzialmente un getter con body e/o un setter con body. In molti casi, il setter e il getter di una proprietà esegue il wrapping di un campo dati, come la proprietà Name del tipo di dispositivo sopra. Questo modello può essere fastidioso quando un tipo ha molte proprietà per il wrapping dei campi di dati, quindi C# 3.0 introduce lo zucchero sintattico della proprietà automatica:

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

La definizione del campo di supporto e il corpo di getter/setter sono generati dal compilatore:

internal class CompiledDevice
{
    [CompilerGenerated]
    private decimal priceBackingField;

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

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

    // Other members.
}

Da C# 6.0, la proprietà auto può essere solo getter:

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

    internal string Name { get; }
}

La proprietà Name sopra viene compilata per avere solo getter e il campo di supporto diventa di sola lettura:

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; }
    }
}

Inizializzatore di proprietà

C# 6.0 introduce lo zucchero sintattico dell'inizializzatore di proprietà, in modo che il valore iniziale della proprietà possa essere fornito in linea:

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

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

L'inizializzatore della proprietà viene compilato nell'inizializzatore del campo di supporto:

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; }
    }
}

Inizializzatore oggetto

Un'istanza di dispositivo può essere inizializzata con una sequenza di istruzioni di assegnazione di proprietà imperative:

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

C# 3.0 introduce lo zucchero sintattico dell'inizializzatore di oggetti, sopra il costruttore di chiamate e il codice delle proprietà impostate può essere unito in uno stile dichiarativo:

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

La sintassi dell'inizializzatore dell'oggetto nel secondo esempio viene compilata in una sequenza di assegnazioni nel primo esempio.

Inizializzatore di raccolta

Allo stesso modo, C# 3,0 introduce anche lo zucchero sintattico dell'inizializzatore di raccolta per il tipo che implementa l'interfaccia System.Collections.IEnumerable e dispone di un metodo Add con parametri. Prendi come esempio la seguente raccolta di dispositivi:

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();
    }
}

Può essere inizializzato anche dichiarativamente:

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

Il codice precedente viene compilato in una normale chiamata al costruttore seguita da una sequenza di chiamate al metodo Add:

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

Inizializzatore dell'indice

C# 6.0 introduce l'inizializzatore di indice per il tipo con setter dell'indicizzatore:

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

È un altro zucchero sintattico dichiarativo:

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

La sintassi precedente viene compilata in una normale chiamata al costruttore seguita da una sequenza di chiamate all'indicizzatore:

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

Operatore di coalescenza nullo

C# 2.0 introduce un operatore di coalescenza nullo ??. Funziona con 2 operandi come a sinistra?? Giusto. Se l'operando sinistro non è nullo, restituisce l'operando sinistro, altrimenti restituisce l'operando destro. Ad esempio, quando si lavora con un valore di riferimento o nullable, è molto comune avere un controllo null in fase di esecuzione e sostituire null:

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;
}

Questo può essere semplificato con l'operatore di coalescenza nullo:

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

Operatori condizionali nulli

È anche molto comune controllare null prima dell'accesso del membro o dell'indicizzatore:

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 introduce operatori condizionali nulli (chiamati anche operatori di propagazione nulli), ?. per l'accesso ai membri e ?[] per l'accesso all'indicizzatore, per semplificare:

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

lancio dell'espressione

A partire da C# 7.0, l'istruzione throw può essere usata come espressione. L'espressione throw viene spesso utilizzata con l'operatore condizionale e sopra l'operatore di coalescenza nullo per semplificare il controllo degli argomenti:

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; }
}

Filtro eccezioni

In C#, era comune catturare un'eccezione, filtrare e quindi gestire/rilanciare. L'esempio seguente tenta di scaricare la stringa HTML dall'URI specificato e può gestire l'errore di download se è presente lo stato della risposta di richiesta non valida. Quindi cattura l'eccezione da controllare. Se l'eccezione ha informazioni previste, gestisce l'eccezione; in caso contrario, genera nuovamente l'eccezione.

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 introduce il filtro delle eccezioni a livello di lingua. il blocco catch può avere un'espressione per filtrare l'eccezione specificata prima di catturare. Se l'espressione restituisce true, viene eseguito il blocco catch:

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.
    }
}

Il filtro di eccezione non è uno zucchero sintattico, ma una funzionalità CLR. L'espressione di filtro delle eccezioni sopra viene compilata per filtrare la clausola in CIL. Il seguente CIL pulito mostra virtualmente il risultato della compilazione:

.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.
  }
}

Quando l'espressione del filtro restituisce false, la clausola catch non viene mai eseguita, quindi non è necessario generare nuovamente l'eccezione. Il rilancio dell'eccezione provoca la rimozione dello stack, come se l'eccezione provenisse dall'istruzione throw e lo stack di chiamate originale e altre informazioni andrebbero perse. Quindi questa funzione è molto utile per la diagnostica e il debug.

Interpolazione di stringhe

Per molti anni, la formattazione composita di stringhe è ampiamente utilizzata in C#. Inserisce valori nei segnaposto indicizzati in formato stringa:

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 introduce lo zucchero sintattico dell'interpolazione delle stringhe per dichiarare i valori in atto, senza mantenere gli ordini separatamente:

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

La seconda versione di interpolazione è più dichiarativa e produttiva, senza mantenere una serie di indici. Questa sintassi è in realtà compilata nella prima formattazione composita.

nomedell'operatore

C# 6.0 introduce un operatore nameof per ottenere il nome stringa di variabile, tipo o membro. Prendi il controllo degli argomenti come esempio:

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

Il nome del parametro è una stringa codificata e non può essere verificata dal compilatore. Ora con l'operatore nameof, il compilatore può generare la costante stringa del nome del parametro sopra:

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

Separatore di cifre e sottolineatura iniziale

C# 7.0 introduce il carattere di sottolineatura come separatore di cifre, nonché il prefisso 0b per il numero binario. C# 7.1 supporta un carattere di sottolineatura facoltativo all'inizio del numero.

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.
}

Queste piccole caratteristiche migliorano notevolmente la leggibilità dei numeri lunghi e binari in fase di progettazione.

Riepilogo

Questo capitolo illustra le conoscenze fondamentali e importanti di C#, come il tipo di riferimento, il tipo di valore, il tipo generico, il tipo di valore nullable e alcune sintassi di base di inizializzatori, operatori, espressioni e così via, incluse alcune nuove sintassi introdotte nelle recenti versioni di C#. Dopo aver acquisito familiarità con queste nozioni di base, i lettori dovrebbero essere pronti per approfondire altri argomenti avanzati del linguaggio C#, della programmazione funzionale e di LINQ.