Domanda

Di recente ho preso in considerazione F # e, anche se non ho intenzione di saltare presto la recinzione, evidenzia sicuramente alcune aree in cui C # (o il supporto della libreria) potrebbe semplificare la vita.

In particolare, sto pensando alla capacità di corrispondenza dei pattern di F #, che consente una sintassi molto ricca - molto più espressiva degli attuali switch / equivalenti C #. Non proverò a fare un esempio diretto (il mio F # non è all'altezza), ma in breve permette:

  • abbina per tipo (con verifica della copertura completa per i sindacati discriminati) [nota che questo determina anche il tipo per la variabile associata, dando accesso ai membri ecc.]
  • corrispondenza per predicato
  • combinazioni di quanto sopra (e forse alcuni altri scenari di cui non sono a conoscenza)

Mentre sarebbe bello per C # eventualmente prendere in prestito [ahem] parte di questa ricchezza, nel frattempo ho visto cosa si può fare in fase di esecuzione - ad esempio, è abbastanza facile mettere insieme alcuni oggetti per permettere:

var getRentPrice = new Switch<Vehicle, int>()
        .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle
        .Case<Bicycle>(30) // returns a constant
        .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
        .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
        .ElseThrow(); // or could use a Default(...) terminator

dove getRentPrice è un Func < Vehicle, int > ;.

[nota - forse Switch / Case qui è i termini sbagliati ... ma mostra l'idea]

Per me, questo è molto più chiaro dell'equivalente usando ripetuto if / else, o un condizionale ternario composito (che diventa molto caotico per le espressioni non banali - parentesi a bizzeffe). Evita anche un lotto di casting e consente una semplice estensione (direttamente o tramite metodi di estensione) a corrispondenze più specifiche, ad esempio una corrispondenza InRange (...) paragonabile a VB Select. ..Case & Quot; x A y & Quot; utilizzo.

Sto solo cercando di valutare se la gente pensa che ci siano molti benefici da costrutti come sopra (in assenza di supporto linguistico)?

Nota inoltre che ho giocato con 3 varianti di cui sopra:

  • a Func < TSource, TValue > versione per la valutazione - paragonabile alle dichiarazioni condizionali ternarie composte
  • un'azione < TSource > versione - paragonabile a if / else if / else if / else if / else
  • un'espressione < Func < TSource, TValue > > versione - come la prima, ma utilizzabile da provider LINQ arbitrari

Inoltre, l'uso della versione basata su Expression consente di riscrivere l'albero di Expression, essenzialmente integrando tutti i rami in una singola espressione condizionale composita, piuttosto che usare ripetute invocazioni. Non ho verificato di recente, ma in alcune build di Entity Framework sembra ricordare che ciò sia necessario, poiché InvocationExpression non è piaciuto molto. Inoltre, consente un utilizzo più efficiente con LINQ-to-Objects, poiché evita ripetute invocazioni di delegati: i test mostrano una corrispondenza come quella sopra (utilizzando il modulo Espressione) che funziona alla stessa velocità [marginalmente più veloce, in effetti] rispetto all'equivalente C # istruzione condizionale composita. Per completezza, Func & Lt; ... & Gt; la versione basata ha richiesto 4 volte più tempo dell'istruzione condizionale C #, ma è ancora molto rapida ed è improbabile che rappresenti un grosso collo di bottiglia nella maggior parte dei casi d'uso.

Accolgo con favore qualsiasi pensiero / input / critica / ecc. su quanto sopra (o sulle possibilità di un supporto linguistico C # più ricco ... ecco sperando ;-p).

È stato utile?

Soluzione

So che è un vecchio argomento, ma in c # 7 puoi fare:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

Altri suggerimenti

Il eccellente blog di Bart De Smet ha una serie di 8 parti che fa esattamente ciò che tu descrivi. Trova la prima parte qui .

Dopo aver provato a fare tale " funzionale " cose in C # (e anche tentando un libro su di esso), sono giunto alla conclusione che no, con poche eccezioni, tali cose non aiutano troppo.

Il motivo principale è che linguaggi come F # ottengono molto del loro potere nel supportare veramente queste funzionalità. Non & Quot; puoi farlo & Quot ;, ma & Quot; è semplice, è chiaro, è previsto & Quot ;.

Ad esempio, nella corrispondenza dei modelli, il compilatore ti dice se c'è una corrispondenza incompleta o quando un'altra partita non verrà mai colpita. Questo è meno utile con i tipi aperti, ma quando si abbina un'unione o tuple discriminate, è molto elegante. In F #, ti aspetti che le persone modellino la corrispondenza e ha immediatamente senso.

Il " problema " è che una volta che inizi a utilizzare alcuni concetti funzionali, è naturale voler continuare. Tuttavia, sfruttando le tuple, le funzioni, l'applicazione parziale del metodo e il curry, la corrispondenza dei modelli, le funzioni nidificate, i generici, il supporto della monade, ecc. In C # diventa molto brutto, molto rapidamente. È divertente e alcune persone molto intelligenti hanno fatto cose molto interessanti in C #, ma in realtà usando sembra pesante.

Quello che ho finito per usare spesso (tra progetti) in C #:

  • Funzioni di sequenza, tramite metodi di estensione per IEnumerable. Cose come ForEach o Process (& Quot; Apply & Quot ;? - eseguono un'azione su un elemento della sequenza mentre è elencato) si adattano perché la sintassi C # la supporta bene.
  • Astrazione di schemi di dichiarazioni comuni. Blocchi complicati try / catch / finally o altri blocchi di codice coinvolti (spesso fortemente generici). L'estensione di LINQ-to-SQL si adatta anche qui.
  • Tuple, in una certa misura.

** Ma nota: la mancanza di generalizzazione automatica e inferenza del tipo ostacolano davvero l'uso anche di queste funzionalità. **

Tutto ciò ha detto, come ha detto qualcun altro, in una piccola squadra, per uno scopo specifico, sì, forse possono aiutarti se sei bloccato con C #. Ma nella mia esperienza, di solito sembravano più seccature di quanto valessero - YMMV.

Alcuni altri collegamenti:

Probabilmente il motivo per cui C # non semplifica l'attivazione del tipo è perché si tratta principalmente di un linguaggio orientato agli oggetti, e il modo "corretto" di farlo in termini orientati agli oggetti sarebbe definire un metodo GetRentPrice sul veicolo e sovrascriverlo in classi derivate.

Detto questo, ho trascorso un po 'di tempo a giocare con linguaggi multi-paradigma e funzionali come F # e Haskell che hanno questo tipo di capacità, e mi sono imbattuto in diversi posti in cui sarebbe stato utile prima ( ad esempio quando non stai scrivendo i tipi che devi attivare in modo da non poter implementare un metodo virtuale su di essi) ed è qualcosa che accetterei con piacere nella lingua insieme ai sindacati discriminati.

[Modifica: è stata rimossa la parte relativa all'esibizione poiché Marc ha indicato che potrebbe essere in cortocircuito]

Un altro potenziale problema è quello dell'usabilità: dall'ultima chiamata è chiaro cosa succede se la partita non soddisfa alcuna condizione, ma qual è il comportamento se corrisponde a due o più condizioni? Dovrebbe generare un'eccezione? Dovrebbe restituire la prima o l'ultima partita?

Un modo che tendo a usare per risolvere questo tipo di problema è usare un campo dizionario con il tipo come chiave e lambda come valore, che è piuttosto conciso da costruire usando la sintassi dell'inizializzatore di oggetti; tuttavia, ciò rappresenta solo il tipo concreto e non consente ulteriori predicati, quindi potrebbe non essere adatto a casi più complessi. [Nota a margine: se guardi l'output del compilatore C #, spesso converte le istruzioni switch in tabelle jump basate su dizionario, quindi non sembra esserci una buona ragione per cui non potrebbe supportare l'attivazione dei tipi]

Non credo che questo tipo di librerie (che si comportano come estensioni di lingua) possano ottenere ampia accettazione, ma sono divertenti da giocare e possono essere davvero utili per i piccoli team che lavorano in domini specifici dove questo è utile . Ad esempio, se stai scrivendo tonnellate di "regole / logiche aziendali" che fanno test di tipo arbitrari come questo e quant'altro, posso vedere come sarebbe utile.

Non ho idea se questa sia probabilmente una caratteristica del linguaggio C # (sembra dubbio, ma chi può vedere il futuro?).

Per riferimento, l'F # corrispondente è approssimativamente:

let getRentPrice (v : Vehicle) = 
    match v with
    | :? Motorcycle as bike -> 100 + bike.Cylinders * 10
    | :? Bicycle -> 30
    | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20
    | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20
    | _ -> failwith "blah"

supponendo che tu abbia definito una gerarchia di classi lungo le linee di

type Vehicle() = class end

type Motorcycle(cyl : int) = 
    inherit Vehicle()
    member this.Cylinders = cyl

type Bicycle() = inherit Vehicle()

type EngineType = Diesel | Gasoline

type Car(engType : EngineType, doors : int) = 
    inherit Vehicle()
    member this.EngineType = engType
    member this.Doors = doors

Per rispondere alla tua domanda, sì, penso che i costrutti sintattici che corrispondono al modello siano utili. Io per uno vorrei vedere il supporto sintattico in C # per questo.

Ecco la mia implementazione di una classe che fornisce (quasi) la stessa sintassi che descrivi

public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o), 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T, 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> Default(Output o)
    {
        return Default(x => o);
    }

    public Output Match(Object o)
    {
        foreach (var tuple in cases)
            if (tuple.Item1(o))
                return tuple.Item2(o);
        throw new Exception("Failed to match");
    }
}

Ecco un codice di prova:

    public enum EngineType
    {
        Diesel,
        Gasoline
    }

    public class Bicycle
    {
        public int Cylinders;
    }

    public class Car
    {
        public EngineType EngineType;
        public int Doors;
    }

    public class MotorCycle
    {
        public int Cylinders;
    }

    public void Run()
    {
        var getRentPrice = new PatternMatcher<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) 
            .Case<Bicycle>(30) 
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
            .Default(0);

        var vehicles = new object[] {
            new Car { EngineType = EngineType.Diesel, Doors = 2 },
            new Car { EngineType = EngineType.Diesel, Doors = 4 },
            new Car { EngineType = EngineType.Gasoline, Doors = 3 },
            new Car { EngineType = EngineType.Gasoline, Doors = 5 },
            new Bicycle(),
            new MotorCycle { Cylinders = 2 },
            new MotorCycle { Cylinders = 3 },
        };

        foreach (var v in vehicles)
        {
            Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v));
        }
    }

Corrispondenza dei motivi (come descritto qui ), il suo scopo è di decostruire i valori in base a la loro specifica del tipo. Tuttavia, il concetto di una classe (o tipo) in C # non è d'accordo con te.

Non c'è niente di sbagliato nel design del linguaggio multi-paradigma, al contrario, è molto bello avere lambda in C #, e Haskell può fare cose imperative per es. IO. Ma non è una soluzione molto elegante, non alla moda di Haskell.

Ma poiché i linguaggi di programmazione procedurale sequenziale possono essere compresi in termini di calcolo lambda e C # sembra adattarsi bene ai parametri di un linguaggio procedurale sequenziale, è una buona scelta. Ma, prendere qualcosa dal puro contesto funzionale di dire Haskell, e poi mettere quella caratteristica in un linguaggio che non è puro, beh, facendo proprio questo, non garantirà un risultato migliore.

Il mio punto è questo, ciò che rende il tick di corrispondenza dei pattern è legato al design del linguaggio e al modello di dati. Detto questo, non credo che la corrispondenza dei pattern sia una caratteristica utile di C # perché non risolve i tipici problemi di C # né si adatta bene al paradigma di programmazione imperativa.

IMHO il modo OO di fare queste cose è il modello Visitatore. I metodi del membro visitatore agiscono semplicemente come costrutti del caso e si lascia che la lingua stessa gestisca l'invio appropriato senza dover & Quot; peek & Quot; a tipi.

Anche se non è molto 'C-sharpey' per attivare il tipo, so che il costrutto sarebbe piuttosto utile in uso generale - ho almeno un progetto personale che potrebbe usarlo (anche se il suo ATM gestibile). C'è un grosso problema di compilazione delle prestazioni, con l'albero di espressione riscritto?

Penso che questo appaia davvero interessante (+1), ma una cosa da fare attenzione: il compilatore C # è abbastanza bravo nell'ottimizzare le istruzioni switch. Non solo per i cortocircuiti: ottieni IL completamente diverso a seconda di quanti casi hai e così via.

Il tuo esempio specifico fa qualcosa che troverei molto utile: non esiste una sintassi equivalente a caso per tipo, poiché (ad esempio) typeof(Motorcycle) non è una costante.

Questo diventa più interessante nell'applicazione dinamica - la tua logica qui potrebbe essere facilmente guidata dai dati, dando esecuzione in stile "motore di regole".

Puoi ottenere ciò che cerchi utilizzando una libreria che ho scritto, chiamato OneOf

Il principale vantaggio rispetto a switch (e if e exceptions as control flow) è che è sicuro in fase di compilazione - non esiste un gestore predefinito o fallisce

   OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types
   var getRentPrice = vehicle
        .Match(
            bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle
            bike => 30, // returns a constant
            car => car.EngineType.Match(
                diesel => 220 + car.Doors * 20
                petrol => 200 + car.Doors * 20
            )
        );

È su Nuget e punta a net451 e netstandard1.6

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top