Programmazione funzionale C# approfondita (15) Corrispondenza di modelli

Programmazione funzionale C# approfondita (15) Corrispondenza di modelli

[LINQ tramite serie C#]

[Serie di approfondimento programmazione funzionale C#]

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

La corrispondenza dei modelli è una caratteristica comune nei linguaggi funzionali. C# 7.0 introduce la corrispondenza dei modelli di base, incluso il valore costante come modello e il tipo come modello, e C# 7.1 supporta i generici nella corrispondenza dei modelli.

Corrispondenza del modello con l'espressione is

Prima di C# 7,0, la parola chiave is viene utilizzata nell'istanza è l'espressione di tipo per verificare se l'istanza è compatibile con il tipo specificato. Da C# 7.0, è in grado di testare il modello costante, inclusi null, valore costante, enumerazione:

internal static partial class PatternMatching
{
    internal static void IsConstantValue(object @object)
    {
        // Type test:
        bool test1 = @object is string;
        // Constant pattern test:
        bool test5 = @object is null; // Compiled to: @object == null
        bool test6 = @object is default; // Compiled to: @object == null
        bool test2 = @object is int.MinValue; // Compiled to: object.Equals(int.MinValue, @object)
        bool test3 = @object is DayOfWeek.Monday; // Compiled to: object.Equals(DayOfWeek.Monday, @object)
        bool test4 = @object is "test"; // Compiled to: object.Equals("test", @object)
    }
}

Le espressioni is per il test nullo vengono semplicemente compilate per il controllo nullo. gli altri casi vengono compilati in chiamate al metodo statico object.Equal, dove il valore costante è il primo argomento e l'istanza testata è il secondo argomento. Internamente, object.Equals esegue prima alcuni controlli, quindi potrebbe chiamare il metodo di istanza Equals del primo argomento:

namespace System
{
    [Serializable]
    public class Object
    {
        public static bool Equals(object objA, object objB) =>
            objA == objB || (objA != null && objB != null && objA.Equals(objB));

        public virtual bool Equals(object obj) =>
            RuntimeHelpers.Equals(this, obj);

        // Other members.
    }
}

Le prime versioni del compilatore C# 7,0 accettano l'istanza testata come primo argomento della chiamata object.Equals e il valore costante come secondo argomento. Questo può avere problemi. In questo modo, l'oggetto statico generato.Equals chiama il metodo di istanza Equals dell'istanza testata. Poiché l'istanza testata può essere qualsiasi tipo personalizzato e il relativo metodo di istanza Equals può essere sovrascritto con un'implementazione personalizzata arbitraria. Nella versione C# 7.0 GA, questo problema veniva risolto facendo in modo che il valore costante diventasse il primo argomento di object.Equals, in modo che fosse possibile chiamare il metodo di istanza Equals del valore costante, che ha un comportamento più prevedibile.

Il modello può anche essere un tipo, seguito da una variabile del modello di quel tipo:

internal static void IsReferenceType(object @object)
{
    if (@object is Uri uri)
    {
        uri.AbsoluteUri.WriteLine();
    }
}

Il tipo nel modello sopra è un tipo di riferimento (classe), quindi l'espressione is viene compilata come conversione di tipo e controllo nullo:

internal static void CompiledIsReferenceType(object @object)
{
    Uri uri = @object as Uri;
    if (uri != null)
    {
        uri.AbsoluteUri.WriteLine();
    }
}

Questo zucchero sintattico funziona anche per il tipo di valore:

internal static void IsValueType(object @object)
{
    if (@object is DateTime dateTime)
    {
        dateTime.ToString("o").WriteLine();
    }
}

L'operatore as non può essere utilizzato per il tipo di valore. L'istanza di tipo cast (ValueType) può funzionare, ma quando il cast non riesce genera un'eccezione. Quindi la corrispondenza del modello per il tipo di valore viene compilata in una conversione del tipo di valore nullable con as operatore e controllo HasValue:

internal static void CompiledIsValueType(object @object)
{
    DateTime? nullableDateTime = @object as DateTime?;
    DateTime dateTime = nullableDateTime.GetValueOrDefault();
    if (nullableDateTime.HasValue)
    {
        dateTime.ToString("o").WriteLine();
    }
}

È anche comune utilizzare la corrispondenza dei modelli con condizioni aggiuntive:

internal static void IsWithCondition(object @object)
{
    if (@object is string @string && TimeSpan.TryParse(@string, out TimeSpan timeSpan))
    {
        timeSpan.TotalMilliseconds.WriteLine();
    }
}

Dopo la compilazione, la condizione è aggiuntiva al controllo nullo:

internal static void CompiledIsWithCondition(object @object)
{
    string @string = @object as string;
    if (@string != null && TimeSpan.TryParse(@string, out TimeSpan timeSpan))
    {
        timeSpan.TotalMilliseconds.WriteLine();
    }
}

Il tipo di dati discusso in precedenza ha la precedenza sul metodo Equals dell'oggetto:

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

Con la sintassi tradizionale, il tipo del parametro oggetto è stato rilevato due volte. In .NET Framework, lo strumento di analisi del codice emette un avviso CA1800 per questo:'obj', un parametro, viene eseguito il cast per digitare 'Data' più volte nel metodo 'Data.Equals(object)'. Memorizza nella cache il risultato dell'operatore 'as' o del cast diretto per eliminare l'istruzione castclass ridondante. Ora con la nuova sintassi, questo può essere semplificato come segue senza preavviso:

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

C# 7.1 supporta i tipi aperti generici nella corrispondenza dei modelli:

internal static void OpenType<T1, T2>(object @object, T1 open1)
{
    if (@object is T1 open) { }
    if (open1 is Uri uri) { }
    if (open1 is T2 open2) { }
}

La parola chiave var può essere il modello di qualsiasi tipo:

internal static void IsType(object @object)
{
    if (@object is var match)
    {
        object.ReferenceEquals(@object, match).WriteLine();
    }
}

Poiché la corrispondenza del modello var funziona sempre, viene compilata su true nella build di debug:

internal static void CompiledIsAnyType(object @object)
{
    object match = @object;
    if (true)
    {
        object.ReferenceEquals(@object, match).WriteLine();
    }
}

Nella build del rilascio, il test if (true) sopra viene semplicemente rimosso.

Corrispondenza del modello con l'istruzione switch

Prima di C# 7.0, l'istruzione switch supporta solo string, tipi integrali (come bool, byte, char, int, long e così via) ed enumerazione; e l'etichetta del caso supporta solo il valore costante. A partire da C# 7,0, switch supporta qualsiasi tipo e l'etichetta case supporta la corrispondenza dei modelli per il valore o il tipo costante. La condizione aggiuntiva per la corrispondenza del modello può essere specificata con una clausola when. L'esempio seguente tenta di convertire l'oggetto in DateTime:

internal static DateTime ToDateTime(object @object)
{
    switch (@object)
    {
        // Match constant @object.
        case null:
            throw new ArgumentNullException(nameof(@object));
        // Match value type.
        case DateTime dateTIme:
            return dateTIme;
        // Match value type with condition.
        case long ticks when ticks >= 0:
            return new DateTime(ticks);
        // Match reference type with condition.
        case string @string when DateTime.TryParse(@string, out DateTime dateTime):
            return dateTime;
        // Match reference type with condition.
        case int[] date when date.Length == 3 && date[0] > 0 && date[1] > 0 && date[2] > 0:
            return new DateTime(year: date[0], month: date[1], day: date[2]);
        // Match reference type.
        case IConvertible convertible:
            return convertible.ToDateTime(provider: null);
        case var _: // default:
            throw new ArgumentOutOfRangeException(nameof(@object));
    }
}

L'ultima sezione con qualsiasi modello di tipo è equivalente alla sezione predefinita, perché corrisponde sempre. Ciascun modello di corrispondenza viene compilato in modo simile all'espressione:

internal static DateTime CompiledToDateTime(object @object)
{
    // case null:
    if (@object == null)
    {
        throw new ArgumentNullException("@object");
    }

    // case DateTime dateTIme:
    DateTime? nullableDateTime = @object as DateTime?;
    DateTime dateTime = nullableDateTime.GetValueOrDefault();
    if (nullableDateTime.HasValue)
    {
        return dateTime;
    }

    // case long ticks
    long? nullableInt64 = @object as long?;
    long ticks = nullableInt64.GetValueOrDefault();
    // when ticks >= 0:
    if (nullableInt64.HasValue && ticks >= 0L)
    {
        return new DateTime(ticks);
    }

    // case string text 
    string @string = @object as string;
    // when DateTime.TryParse(text, out DateTime dateTime):
    if (@string != null && DateTime.TryParse(@string, out DateTime parsedDateTime))
    {
        return parsedDateTime;
    }

    // case int[] date
    int[] date = @object as int[];
    // when date.Length == 3 && date[0] >= 0 && date[1] >= 0 && date[2] >= 0:
    if (date != null && date.Length == 3 && date[0] >= 0 && date[1] >= 0 && date[2] >= 0)
    {
        return new DateTime(date[0], date[1], date[2]);
    }

    // case IConvertible convertible:
    IConvertible convertible = @object as IConvertible;
    if (convertible != null)
    {
        return convertible.ToDateTime(null);
    }

    // case var _:
    // or
    // default:
    throw new ArgumentOutOfRangeException("@object");
}