Esiste un'alternativa migliore di questa al "tipo di accensione"?


331

Visto che C # non può switchsu un tipo (che ho raccolto non è stato aggiunto come caso speciale perché le isrelazioni significano che casepotrebbe essere applicato più di un distinto ), esiste un modo migliore per simulare l'attivazione di un tipo diverso da questo?

void Foo(object o)
{
    if (o is A)
    {
        ((A)o).Hop();
    }
    else if (o is B)
    {
        ((B)o).Skip();
    }
    else
    {
        throw new ArgumentException("Unexpected type: " + o.GetType());
    }
}

18
Per curiosità, perché non usi semplicemente il polimorfismo?

18
@jeyoung ha sigillato le lezioni e non ne vale la pena per situazioni ad hoc
xyz



2
@jeyoung: una situazione tipica in cui il polimorfismo non può essere utilizzato è quando i tipi che vengono commutati non devono conoscere il codice che contiene l' switchistruzione. Un esempio: l'assieme A contiene una serie di oggetti dati (che non cambierà, definiti in un documento di specifica o simili). Gli assiemi B , C e D ogni riferimento A e forniscono una conversione per i vari oggetti dati da A (ad esempio una serializzazione / deserializzazione in un determinato formato). O devi rispecchiare l'intera gerarchia di classi in B , C e D e usare le fabbriche, oppure devi ...
OR Mapper,

Risposte:


276

L'accensione dei tipi è sicuramente carente in C # ( AGGIORNAMENTO: in C # 7 / VS 2017 è supportata l'attivazione dei tipi - vedi la risposta di Zachary Yates di seguito ). Per fare ciò senza una grande istruzione if / else if / else, dovrai lavorare con una struttura diversa. Ho scritto un post sul blog qualche tempo fa in dettaglio come costruire una struttura TypeSwitch.

https://docs.microsoft.com/archive/blogs/jaredpar/switching-on-types

Versione breve: TypeSwitch è progettato per impedire il cast ridondante e fornire una sintassi simile a una normale istruzione switch / case. Ad esempio, ecco TypeSwitch in azione su un evento standard di Windows Form

TypeSwitch.Do(
    sender,
    TypeSwitch.Case<Button>(() => textBox1.Text = "Hit a Button"),
    TypeSwitch.Case<CheckBox>(x => textBox1.Text = "Checkbox is " + x.Checked),
    TypeSwitch.Default(() => textBox1.Text = "Not sure what is hovered over"));

Il codice per TypeSwitch è in realtà piuttosto piccolo e può essere facilmente inserito nel tuo progetto.

static class TypeSwitch {
    public class CaseInfo {
        public bool IsDefault { get; set; }
        public Type Target { get; set; }
        public Action<object> Action { get; set; }
    }

    public static void Do(object source, params CaseInfo[] cases) {
        var type = source.GetType();
        foreach (var entry in cases) {
            if (entry.IsDefault || entry.Target.IsAssignableFrom(type)) {
                entry.Action(source);
                break;
            }
        }
    }

    public static CaseInfo Case<T>(Action action) {
        return new CaseInfo() {
            Action = x => action(),
            Target = typeof(T)
        };
    }

    public static CaseInfo Case<T>(Action<T> action) {
        return new CaseInfo() {
            Action = (x) => action((T)x),
            Target = typeof(T)
        };
    }

    public static CaseInfo Default(Action action) {
        return new CaseInfo() {
            Action = x => action(),
            IsDefault = true
        };
    }
}

26
"type == entry.Target" può anche essere modificato in "entry.Target.IsAssignableFrom (type)" per tenere conto dei tipi compatibili (ad es. sottoclassi).
Mark Cidade,

Modificato il codice per utilizzare "entry.Target.IsAssignableFrom (type)" in modo da supportare le sottoclassi.
Matt Howells,

3
Una cosa forse degna di nota è che (da quello che ho capito) è necessario specificare l'ultima azione "predefinita" per assicurarsi che tutti gli altri casi siano controllati. Credo che questo non sia un requisito in uno switch standard - non che io abbia mai visto nessuno provare a impiantare un 'default' in qualsiasi luogo diverso dal basso. Un paio di opzioni fail-safe per questo potrebbero essere ordinare l'array per assicurarsi che il valore predefinito sia ultimo (un po 'dispendioso) o inserire il valore predefinito in una variabile da elaborare dopo il foreach(cosa che succederebbe solo se non fosse stata trovata una corrispondenza)
musefan,

Cosa succede se il mittente è nullo? GetType genererà un'eccezione
Jon

Due suggerimenti: gestire la sorgente null chiamando default o lanciando un'eccezione e sbarazzarsi del valore booleano in CaseInfosemplicemente controllando il valore del tipo (se è null è il valore predefinito).
Felix K.,

291

Con C # 7 , fornito con Visual Studio 2017 (versione 15. *), è possibile utilizzare i tipi nelle caseistruzioni (corrispondenza del modello):

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

Con C # 6, puoi utilizzare un'istruzione switch con l' operatore nameof () (grazie a @Joey Adams):

switch(o.GetType().Name) {
    case nameof(AType):
        break;
    case nameof(BType):
        break;
}

Con C # 5 e precedenti, potresti usare un'istruzione switch, ma dovrai usare una stringa magica contenente il nome del tipo ... che non è particolarmente adatto ai refactor (grazie a @nukefusion)

switch(o.GetType().Name) {
  case "AType":
    break;
}

1
funziona con caseof di tipo (stringa) .Nome: ... o deve essere con Valuetype?
Tomer W,

3
L'offuscamento può distruggerlo
Konrad Morawski,

6
@nukefusion: Cioè, a meno che tu non usi il nuovo nameof()operatore lucido .
Joey Adams,

21
Non mi piace questa risposta perché nameof (NamespaceA.ClassC) == nameof (NamespaceB.ClassC) è vero.
Ischas,

7
(c # 7) puoi anche usare il trattino basso se non hai bisogno di accedere all'oggetto:case UnauthorizedException _:
Assaf S.

101

Un'opzione è quella di avere un dizionario da Typea Action(o qualche altro delegato). Cerca l'azione in base al tipo, quindi eseguila. L'ho usato per le fabbriche prima d'ora.


31
Nota minore: buona per le partite 1: 1, ma potrebbe essere una seccatura con ereditarietà e / o interfacce, soprattutto perché non è garantito che l'ordine venga conservato con un dizionario. Ma comunque, è il modo in cui lo faccio in pochi posti ;-p Quindi +1
Marc Gravell

@Marc: in che modo l'ereditarietà o le interfacce si rompono in questo paradigma? Supponendo che la chiave sia un tipo e l'azione sia un metodo, l'ereditarietà o le interfacce dovrebbero effettivamente forzare la cosa giusta (TM), per quanto ne so. Sicuramente capisco il problema con molteplici azioni e mancanza di ordini.
Harper Shelby,

2
Ho usato questa tecnica molto in passato, di solito prima di trasferirmi in un contenitore IoC
Chris Canal,

4
Questa tecnica si interrompe per ereditarietà e interfacce perché è necessaria una corrispondenza uno a uno tra l'oggetto che si sta controllando e il delegato che si sta chiamando. Quale delle interfacce multiple di un oggetto dovresti provare a trovare nel dizionario?
Robert Rossney,

5
Se stai costruendo un dizionario appositamente per questo scopo, potresti sovraccaricare l'indicizzatore per restituire il valore del tipo di chiave, o se manca la sua superclasse, se manca allora quella superclasse, ecc., Fino a quando non rimane più nulla.
Erik Forbes,

49

Con la risposta di JaredPar nella parte posteriore della mia testa, ho scritto una variante della sua TypeSwitchclasse che utilizza l'inferenza del tipo per una sintassi più gradevole:

class A { string Name { get; } }
class B : A { string LongName { get; } }
class C : A { string FullName { get; } }
class X { public string ToString(IFormatProvider provider); }
class Y { public string GetIdentifier(); }

public string GetName(object value)
{
    string name = null;
    TypeSwitch.On(value)
        .Case((C x) => name = x.FullName)
        .Case((B x) => name = x.LongName)
        .Case((A x) => name = x.Name)
        .Case((X x) => name = x.ToString(CultureInfo.CurrentCulture))
        .Case((Y x) => name = x.GetIdentifier())
        .Default((x) => name = x.ToString());
    return name;
}

Si noti che l'ordine dei Case()metodi è importante.


Ottieni il codice completo e commentato per la mia TypeSwitchclasse . Questa è una versione abbreviata funzionante:

public static class TypeSwitch
{
    public static Switch<TSource> On<TSource>(TSource value)
    {
        return new Switch<TSource>(value);
    }

    public sealed class Switch<TSource>
    {
        private readonly TSource value;
        private bool handled = false;

        internal Switch(TSource value)
        {
            this.value = value;
        }

        public Switch<TSource> Case<TTarget>(Action<TTarget> action)
            where TTarget : TSource
        {
            if (!this.handled && this.value is TTarget)
            {
                action((TTarget) this.value);
                this.handled = true;
            }
            return this;
        }

        public void Default(Action<TSource> action)
        {
            if (!this.handled)
                action(this.value);
        }
    }
}

Sembra una buona soluzione e volevo vedere cos'altro avevi da dire a riguardo ma il blog è morto.
Wes Grant,

1
Accidenti, hai ragione. Il mio webhost ha dei problemi da un'ora. Ci stanno lavorando. Il post sul mio blog è essenzialmente lo stesso della risposta qui, ma con un link al codice sorgente completo.
Daniel AA Pelsmaeker,

1
Adoro come questo riduca un sacco di parentesi if a un semplice interruttore "funzionale". Bel lavoro!
James White,

2
È inoltre possibile aggiungere un metodo di estensione per il caso iniziale: public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSource. Questo ti permette di direvalue.Case((C x) ...
Joey Adams,

1
@JoeyAdams: ho incorporato il tuo ultimo suggerimento, insieme ad alcuni piccoli miglioramenti. Tuttavia, sto mantenendo la sintassi la stessa.
Daniel AA Pelsmaeker,

14

Crea una superclasse (S) e fai in modo che A e B ereditino da essa. Quindi dichiarare un metodo astratto su S che ogni sottoclasse deve implementare.

In questo modo il metodo "foo" può anche cambiare la sua firma in Foo (S o), rendendolo sicuro e non è necessario lanciare quella brutta eccezione.


Vero bruno, ma la domanda non lo suggerisce. Potresti includerlo nella tua risposta anche se Pablo.
Dana the Sane,

Dalla domanda penso che A e B siano abbastanza generici da poter essere A = String; B = Elenco <int> per esempio ...
bruno conde,

13

È possibile utilizzare la corrispondenza dei motivi in ​​C # 7 o versioni successive:

switch (foo.GetType())
{
    case var type when type == typeof(Player):
        break;
    case var type when type == typeof(Address):
        break;
    case var type when type == typeof(Department):
        break;
    case var type when type == typeof(ContactType):
        break;
    default:
        break;
}

Grazie per questo! Può essere utilizzato anche per rilevare sottoclassi: if (this.TemplatedParent.GetType (). IsSubclassOf (typeof (RadGridView))) può essere modificato in: switch (this.TemplatedParent.GetType ()) case var subRadGridView quando subRadGridView.IsSubclassOf ( typeof (RadGridView)):
Flemming Bonde Kentved

Lo stai facendo male. Vedi la risposta di Serge Intern e leggi il principio di sostituzione di Liskov
0xF

8

Dovresti davvero sovraccaricare il tuo metodo, non cercare di fare da solo la chiarimento. La maggior parte delle risposte finora non tiene conto delle sottoclassi future, il che può portare a problemi di manutenzione davvero terribili in seguito.


3
La risoluzione del sovraccarico è determinata staticamente in modo che non funzioni affatto.
Neutrino,

@Neutrino: non c'è nulla nella domanda che imponga che il tipo non sia noto al momento della compilazione. E se lo è, un sovraccarico ha molto più senso di qualsiasi altra opzione, dato l'esempio di codice originale dell'OP.
Peter Duniho,

Penso che il fatto che stia cercando di usare un'istruzione 'if' o 'switch' per determinare il tipo è un'indicazione abbastanza chiara che il tipo non è noto al momento della compilazione.
Neutrino,

@Neutrino, ti ricordo che, come ha sottolineato Sergey Berezovskiy, c'è la parola chiave dinamica in C #, che rappresenta un tipo che deve essere risolto dinamicamente (in fase di esecuzione, piuttosto che in fase di compilazione).
Davide Cannizzo,

8

Se si utilizza C # 4, è possibile utilizzare la nuova funzionalità dinamica per ottenere un'alternativa interessante. Non sto dicendo che sia meglio, in effetti sembra molto probabile che sarebbe più lento, ma ha una certa eleganza.

class Thing
{

  void Foo(A a)
  {
     a.Hop();
  }

  void Foo(B b)
  {
     b.Skip();
  }

}

E l'uso:

object aOrB = Get_AOrB();
Thing t = GetThing();
((dynamic)t).Foo(aorB);

Il motivo per cui funziona è che una chiamata al metodo dinamico C # 4 ha i suoi sovraccarichi risolti in fase di esecuzione piuttosto che in fase di compilazione. Ho scritto un po 'di più su questa idea di recente . Ancora una volta, vorrei solo ribadire che questo probabilmente funziona peggio di tutti gli altri suggerimenti, lo sto offrendo semplicemente come una curiosità.


1
Ho avuto la stessa idea oggi. È circa 3 volte più lento rispetto all'attivazione del nome del tipo. Ovviamente il più lento è relativo (per 60.000.000 di chiamate, solo 4 secondi), e il codice è molto più leggibile, ne vale la pena.
Daryl,


7

Per i tipi predefiniti, è possibile utilizzare l'enumerazione TypeCode. Si noti che GetType () è un po 'lento, ma probabilmente non pertinente nella maggior parte delle situazioni.

switch (Type.GetTypeCode(someObject.GetType()))
{
    case TypeCode.Boolean:
        break;
    case TypeCode.Byte:
        break;
    case TypeCode.Char:
        break;
}

Per i tipi personalizzati, è possibile creare la propria enumerazione e un'interfaccia o una classe base con proprietà o metodo astratti ...

Implementazione di classe astratta della proprietà

public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public abstract class Foo
{
    public abstract FooTypes FooType { get; }
}
public class FooFighter : Foo
{
    public override FooTypes FooType { get { return FooTypes.FooFighter; } }
}

Implementazione del metodo in classe astratta

public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public abstract class Foo
{
    public abstract FooTypes GetFooType();
}
public class FooFighter : Foo
{
    public override FooTypes GetFooType() { return FooTypes.FooFighter; }
}

Implementazione dell'interfaccia della proprietà

public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public interface IFooType
{
    FooTypes FooType { get; }
}
public class FooFighter : IFooType
{
    public FooTypes FooType { get { return FooTypes.FooFighter; } }
}

Implementazione dell'interfaccia del metodo

public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public interface IFooType
{
    FooTypes GetFooType();
}
public class FooFighter : IFooType
{
    public FooTypes GetFooType() { return FooTypes.FooFighter; }
}

Anche uno dei miei colleghi me ne ha parlato: ha il vantaggio di poterlo usare letteralmente per qualsiasi tipo di oggetto, non solo per quelli che definisci. Ha lo svantaggio di essere un po 'più grande e più lento.

Per prima cosa definisci una classe statica come questa:

public static class TypeEnumerator
{
    public class TypeEnumeratorException : Exception
    {
        public Type unknownType { get; private set; }
        public TypeEnumeratorException(Type unknownType) : base()
        {
            this.unknownType = unknownType;
        }
    }
    public enum TypeEnumeratorTypes { _int, _string, _Foo, _TcpClient, };
    private static Dictionary<Type, TypeEnumeratorTypes> typeDict;
    static TypeEnumerator()
    {
        typeDict = new Dictionary<Type, TypeEnumeratorTypes>();
        typeDict[typeof(int)] = TypeEnumeratorTypes._int;
        typeDict[typeof(string)] = TypeEnumeratorTypes._string;
        typeDict[typeof(Foo)] = TypeEnumeratorTypes._Foo;
        typeDict[typeof(System.Net.Sockets.TcpClient)] = TypeEnumeratorTypes._TcpClient;
    }
    /// <summary>
    /// Throws NullReferenceException and TypeEnumeratorException</summary>
    /// <exception cref="System.NullReferenceException">NullReferenceException</exception>
    /// <exception cref="MyProject.TypeEnumerator.TypeEnumeratorException">TypeEnumeratorException</exception>
    public static TypeEnumeratorTypes EnumerateType(object theObject)
    {
        try
        {
            return typeDict[theObject.GetType()];
        }
        catch (KeyNotFoundException)
        {
            throw new TypeEnumeratorException(theObject.GetType());
        }
    }
}

E quindi puoi usarlo in questo modo:

switch (TypeEnumerator.EnumerateType(someObject))
{
    case TypeEnumerator.TypeEnumeratorTypes._int:
        break;
    case TypeEnumerator.TypeEnumeratorTypes._string:
        break;
}

Grazie per aver aggiunto TypeCode () - variante per i tipi primitivi, perché anche la variante C # 7.0 - non funziona con quelli (né ovviamente nameof () ovviamente)
Ole Albers,

6

Mi è piaciuto l' uso della tipizzazione implicita da parte di Virtlink per rendere lo switch molto più leggibile, ma non mi piaceva che un avvio anticipato non fosse possibile e che stiamo facendo allocazioni. Alziamo un po 'la perf.

public static class TypeSwitch
{
    public static void On<TV, T1>(TV value, Action<T1> action1)
        where T1 : TV
    {
        if (value is T1) action1((T1)value);
    }

    public static void On<TV, T1, T2>(TV value, Action<T1> action1, Action<T2> action2)
        where T1 : TV where T2 : TV
    {
        if (value is T1) action1((T1)value);
        else if (value is T2) action2((T2)value);
    }

    public static void On<TV, T1, T2, T3>(TV value, Action<T1> action1, Action<T2> action2, Action<T3> action3)
        where T1 : TV where T2 : TV where T3 : TV
    {
        if (value is T1) action1((T1)value);
        else if (value is T2) action2((T2)value);
        else if (value is T3) action3((T3)value);
    }

    // ... etc.
}

Bene, mi fanno male le dita. Facciamolo in T4:

<#@ template debug="false" hostSpecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ import namespace="System.Linq" #> 
<#@ import namespace="System.IO" #> 
<#
    string GenWarning = "// THIS FILE IS GENERATED FROM " + Path.GetFileName(Host.TemplateFile) + " - ANY HAND EDITS WILL BE LOST!";
    const int MaxCases = 15;
#>
<#=GenWarning#>

using System;

public static class TypeSwitch
{
<# for(int icase = 1; icase <= MaxCases; ++icase) {
    var types = string.Join(", ", Enumerable.Range(1, icase).Select(i => "T" + i));
    var actions = string.Join(", ", Enumerable.Range(1, icase).Select(i => string.Format("Action<T{0}> action{0}", i)));
    var wheres = string.Join(" ", Enumerable.Range(1, icase).Select(i => string.Format("where T{0} : TV", i)));
#>
    <#=GenWarning#>

    public static void On<TV, <#=types#>>(TV value, <#=actions#>)
        <#=wheres#>
    {
        if (value is T1) action1((T1)value);
<# for(int i = 2; i <= icase; ++i) { #>
        else if (value is T<#=i#>) action<#=i#>((T<#=i#>)value);
<#}#>
    }

<#}#>
    <#=GenWarning#>
}

Aggiustando un po 'l'esempio di Virtlink:

TypeSwitch.On(operand,
    (C x) => name = x.FullName,
    (B x) => name = x.LongName,
    (A x) => name = x.Name,
    (X x) => name = x.ToString(CultureInfo.CurrentCulture),
    (Y x) => name = x.GetIdentifier(),
    (object x) => name = x.ToString());

Leggibile e veloce. Ora, come tutti sottolineano nelle loro risposte e data la natura di questa domanda, l'ordine è importante nella corrispondenza dei tipi. Perciò:

  • Inserisci prima i tipi di foglia, poi i tipi di base.
  • Per i tipi di pari, metti prima le corrispondenze più probabili per massimizzare la perf.
  • Ciò implica che non è necessario un caso predefinito speciale. Invece, basta usare il tipo più base nella lambda e metterlo per ultimo.

5

Dato che l'ereditarietà facilita il riconoscimento di un oggetto come più di un tipo, penso che un interruttore possa portare a una cattiva ambiguità. Per esempio:

Caso 1

{
  string s = "a";
  if (s is string) Print("Foo");
  else if (s is object) Print("Bar");
}

Caso 2

{
  string s = "a";
  if (s is object) Print("Foo");
  else if (s is string) Print("Bar");
}

Perché s è una stringa e un oggetto. Penso che quando scrivi un messaggio switch(foo)ti aspetti che il foo corrisponda a una e solo una delle caseaffermazioni. Con i tipi switch on, l'ordine in cui si scrivono le dichiarazioni del caso potrebbe cambiare il risultato dell'intera istruzione switch. Penso che sarebbe sbagliato.

Si potrebbe pensare a un controllo del compilatore sui tipi di un'istruzione "typeswitch", verificando che i tipi enumerati non si ereditino l'uno dall'altro. Questo non esiste però.

foo is Tnon è lo stesso di foo.GetType() == typeof(T)!!



4

Un altro modo sarebbe definire un'interfaccia IThing e quindi implementarla in entrambe le classi, ecco lo snipet:

public interface IThing
{
    void Move();
}

public class ThingA : IThing
{
    public void Move()
    {
        Hop();
    }

    public void Hop(){  
        //Implementation of Hop 
    }

}

public class ThingA : IThing
{
    public void Move()
    {
        Skip();
    }

    public void Skip(){ 
        //Implementation of Skip    
    }

}

public class Foo
{
    static void Main(String[] args)
    {

    }

    private void Foo(IThing a)
    {
        a.Move();
    }
}

4

Secondo la specifica C # 7.0, è possibile dichiarare una variabile locale con ambito in a casedi switch:

object a = "Hello world";
switch (a)
{
    case string myString:
        // The variable 'a' is a string!
        break;
    case int myInt:
        // The variable 'a' is an int!
        break;
    case Foo myFoo:
        // The variable 'a' is of type Foo!
        break;
}

Questo è il modo migliore per fare una cosa del genere perché implica solo operazioni di casting e push-on-the-stack, che sono le operazioni più veloci che un interprete può eseguire subito dopo operazioni e booleancondizioni bit per bit .

Confrontando questo con un Dictionary<K, V>, ecco molto meno l'utilizzo della memoria: tenere un dizionario richiede più spazio nella RAM e un po 'più di calcolo dalla CPU per creare due array (uno per le chiavi e l'altro per i valori) e raccogliere codici hash per le chiavi da mettere valori alle rispettive chiavi.

Così, per quanto ne so, non credo che un modo più veloce potrebbe esistere a meno che non si desidera utilizzare solo una if- then- elseblocco con l' isoperatore come segue:

object a = "Hello world";
if (a is string)
{
    // The variable 'a' is a string!
} else if (a is int)
{
    // The variable 'a' is an int!
} // etc.

3

È possibile creare metodi sovraccarichi:

void Foo(A a) 
{ 
    a.Hop(); 
}

void Foo(B b) 
{ 
    b.Skip(); 
}

void Foo(object o) 
{ 
    throw new ArgumentException("Unexpected type: " + o.GetType()); 
}

E lancia l'argomento da dynamicdigitare per evitare il controllo statico del tipo:

Foo((dynamic)something);

3

I miglioramenti di C # 8 della corrispondenza dei modelli hanno reso possibile farlo in questo modo. In alcuni casi fa il lavoro e più conciso.

        public Animal Animal { get; set; }
        ...
        var animalName = Animal switch
        {
            Cat cat => "Tom",
            Mouse mouse => "Jerry",
            _ => "unknown"
        };

2

Stai cercando Discriminated Unionsquali sono le funzionalità del linguaggio di F #, ma puoi ottenere un effetto simile usando una libreria che ho creato, chiamato OneOf

https://github.com/mcintyre321/OneOf

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

void Foo(OneOf<A, B> o)
{
    o.Switch(
        a => a.Hop(),
        b => b.Skip()
    );
}

Se aggiungi un terzo elemento a o, otterrai un errore del compilatore poiché devi aggiungere un gestore Func all'interno della chiamata switch.

Puoi anche fare un .Matchvalore che restituisce un valore, anziché eseguire un'istruzione:

double Area(OneOf<Square, Circle> o)
{
    return o.Match(
        square => square.Length * square.Length,
        circle => Math.PI * circle.Radius * circle.Radius
    );
}

2

Crea un'interfaccia IFooable, quindi crea tu Ae le Bclassi per implementare un metodo comune, che a sua volta chiama il metodo corrispondente che desideri:

interface IFooable
{
    public void Foo();
}

class A : IFooable
{
    //other methods ...

    public void Foo()
    {
        this.Hop();
    }
}

class B : IFooable
{
    //other methods ...

    public void Foo()
    {
        this.Skip();
    }
}

class ProcessingClass
{
    public void Foo(object o)
    {
        if (o == null)
            throw new NullRefferenceException("Null reference", "o");

        IFooable f = o as IFooable;
        if (f != null)
        {
            f.Foo();
        }
        else
        {
            throw new ArgumentException("Unexpected type: " + o.GetType());
        }
    }
}

Nota che è meglio usare asinvece prima controllando ise poi lanciando, in questo modo fai 2 lanci, quindi è più costoso.


2

In questi casi di solito finisco con un elenco di predicati e azioni. Qualcosa del genere:

class Mine {
    static List<Func<object, bool>> predicates;
    static List<Action<object>> actions;

    static Mine() {
        AddAction<A>(o => o.Hop());
        AddAction<B>(o => o.Skip());
    }

    static void AddAction<T>(Action<T> action) {
        predicates.Add(o => o is T);
        actions.Add(o => action((T)o);
    }

    static void RunAction(object o) {
        for (int i=0; o < predicates.Count; i++) {
            if (predicates[i](o)) {
                actions[i](o);
                break;
            }
        }
    }

    void Foo(object o) {
        RunAction(o);
    }
}

2

Dopo aver confrontato le opzioni che alcune risposte fornite qui con le funzionalità di F #, ho scoperto che F # aveva un supporto migliore per la commutazione basata sul tipo (anche se sto ancora aderendo a C #).
Potresti voler vedere qui e qui .


2
<inserire qui la spina per F #>
Overlord Zurg,

1

Vorrei creare un'interfaccia con qualsiasi nome e metodo che abbia senso per il tuo switch, chiamiamoli rispettivamente: IDoableche dice di implementare void Do().

public interface IDoable
{
    void Do();
}

public class A : IDoable
{
    public void Hop() 
    {
        // ...
    }

    public void Do()
    {
        Hop();
    }
}

public class B : IDoable
{
    public void Skip() 
    {
        // ...
    }

    public void Do()
    {
        Skip();
    }
}

e cambia il metodo come segue:

void Foo<T>(T obj)
    where T : IDoable
{
    // ...
    obj.Do();
    // ...
}

Almeno con quello sei al sicuro al momento della compilazione e sospetto che dal punto di vista delle prestazioni sia meglio che controllare il tipo in fase di esecuzione.


1

A partire da C # 8 puoi renderlo ancora più conciso con il nuovo switch. E con l'uso dell'opzione di eliminazione _ puoi evitare di creare variabili innecesary quando non ti servono, in questo modo:

        return document switch {
            Invoice _ => "Is Invoice",
            ShippingList _ => "Is Shipping List",
            _ => "Unknown"
        };

Invoice e ShippingList sono classi e il documento è un oggetto che può essere uno di essi.


0

Concordo con Jon sull'avere un hash di azioni per il nome della classe. Se mantieni il tuo modello, potresti prendere in considerazione l'uso del costrutto "as" invece:

A a = o as A;
if (a != null) {
    a.Hop();
    return;
}
B b = o as B;
if (b != null) {
    b.Skip();
    return;
}
throw new ArgumentException("...");

La differenza è che quando usi lo scalpiccio if (foo is Bar) {((Bar) foo) .Action (); } stai facendo il casting del tipo due volte. Ora forse il compilatore ottimizzerà e funzionerà solo una volta, ma non ci contare.


1
In realtà non mi piacciono più punti di uscita (ritorni), ma se si desidera attenersi a questo, aggiungere "if (o == null) lancio" all'inizio, poiché in seguito non si saprà se il cast non ha esito positivo o se il l'oggetto era nullo.
Sunny Milenov,

0

Come suggerisce Pablo, l'approccio all'interfaccia è quasi sempre la cosa giusta da fare per gestirlo. Per utilizzare davvero switch, un'altra alternativa è avere un enum personalizzato che denota il tuo tipo nelle tue classi.

enum ObjectType { A, B, Default }

interface IIdentifiable
{
    ObjectType Type { get; };
}
class A : IIdentifiable
{
    public ObjectType Type { get { return ObjectType.A; } }
}

class B : IIdentifiable
{
    public ObjectType Type { get { return ObjectType.B; } }
}

void Foo(IIdentifiable o)
{
    switch (o.Type)
    {
        case ObjectType.A:
        case ObjectType.B:
        //......
    }
}

Questo è un po 'implementato anche nel BCL. Un esempio è MemberInfo.MemberTypes , un altro è GetTypeCodeper i tipi primitivi, come:

void Foo(object o)
{
    switch (Type.GetTypeCode(o.GetType())) // for IConvertible, just o.GetTypeCode()
    {
        case TypeCode.Int16:
        case TypeCode.Int32:
        //etc ......
    }
}

0

Questa è una risposta alternativa che mescola i contributi delle risposte JaredPar e VirtLink, con i seguenti vincoli:

  • La costruzione dell'interruttore si comporta come una funzione e riceve le funzioni come parametri per i casi.
  • Assicura che sia costruito correttamente e che esista sempre una funzione predefinita .
  • Si ritorna dopo la prima partita (vale per JaredPar risposta, non è vero per VirtLink uno).

Uso:

 var result = 
   TSwitch<string>
     .On(val)
     .Case((string x) => "is a string")
     .Case((long x) => "is a long")
     .Default(_ => "what is it?");

Codice:

public class TSwitch<TResult>
{
    class CaseInfo<T>
    {
        public Type Target { get; set; }
        public Func<object, T> Func { get; set; }
    }

    private object _source;
    private List<CaseInfo<TResult>> _cases;

    public static TSwitch<TResult> On(object source)
    {
        return new TSwitch<TResult> { 
            _source = source,
            _cases = new List<CaseInfo<TResult>>()
        };
    }

    public TResult Default(Func<object, TResult> defaultFunc)
    {
        var srcType = _source.GetType();
       foreach (var entry in _cases)
            if (entry.Target.IsAssignableFrom(srcType))
                return entry.Func(_source);

        return defaultFunc(_source);
    }

    public TSwitch<TResult> Case<TSource>(Func<TSource, TResult> func)
    {
        _cases.Add(new CaseInfo<TResult>
        {
            Func = x => func((TSource)x),
            Target = typeof(TSource)
        });
        return this;
    }
}

0

Sì, basta usare il "pattern matching" leggermente strano dal C # 7 in poi per abbinare su classe o struttura:

IObject concrete1 = new ObjectImplementation1();
IObject concrete2 = new ObjectImplementation2();

switch (concrete1)
{
    case ObjectImplementation1 c1: return "type 1";         
    case ObjectImplementation2 c2: return "type 2";         
}

0

Io uso

    public T Store<T>()
    {
        Type t = typeof(T);

        if (t == typeof(CategoryDataStore))
            return (T)DependencyService.Get<IDataStore<ItemCategory>>();
        else
            return default(T);
    }

0

Dovrebbe funzionare con

tipo di caso _:

piace:

int i = 1;
bool b = true;
double d = 1.1;
object o = i; // whatever you want

switch (o)
{
    case int _:
        Answer.Content = "You got the int";
        break;
    case double _:
        Answer.Content = "You got the double";
        break;
    case bool _:
        Answer.Content = "You got the bool";
        break;
}

0

Se conosci la classe che ti aspetti ma non hai ancora un oggetto, puoi anche farlo:

private string GetAcceptButtonText<T>() where T : BaseClass, new()
{
    switch (new T())
    {
        case BaseClassReview _: return "Review";
        case BaseClassValidate _: return "Validate";
        case BaseClassAcknowledge _: return "Acknowledge";
        default: return "Accept";
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.