C’è un’alternativa migliore di questa per ‘triggersre il tipo’?

Visto che C # non può triggersre un Tipo (che io raccolgo non è stato aggiunto come caso speciale perché è-una relazione significa che potrebbe applicarsi più di un caso distinto), c’è un modo migliore per simulare l’triggerszione del tipo di 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()); } } 

L’triggerszione dei tipi è decisamente carente in C # ( UPDATE: in C # 7 / VS 2017 l’triggerszione dei tipi è supportata – vedi la risposta di Zachary Yates sotto ). Per fare ciò senza una grande istruzione if / else if / else, dovrai lavorare con una struttura diversa. Ho scritto un post sul blog un po ‘indietro che spiega come build una struttura TypeSwitch.

http://blogs.msdn.com/jaredpar/archive/2008/05/16/switching-on-types.aspx

Versione breve: TypeSwitch è progettato per impedire la trasmissione ridondante e fornire una syntax simile a una normale istruzione switch / caso. Ad esempio, ecco TypeSwitch in azione su un evento di modulo Windows standard

 TypeSwitch.Do( sender, TypeSwitch.Case 

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 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(Action action) { return new CaseInfo() { Action = x => action(), Target = typeof(T) }; } public static CaseInfo Case(Action 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 }; } } 

Con C # 7 , fornito con Visual Studio 2017 (versione 15. *), è ansible utilizzare i tipi nelle istruzioni case (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(""); break; case null: throw new ArgumentNullException(nameof(shape)); } 

Con C # 6, puoi usare 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 che contiene il nome del tipo … che non è particolarmente appropriato per il refattore (grazie a @nukefusion)

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

Un’opzione è avere un dizionario da Type a Action (o qualche altro delegato). Cerca l’azione in base al tipo, quindi eseguila. Ho usato questo per le fabbriche prima d’ora.

Con la risposta di JaredPar nella parte posteriore della mia testa, ho scritto una variante della sua class TypeSwitch che utilizza l’inferenza di tipo per una syntax migliore:

 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 metodi Case() è importante.


Ottieni il codice completo e commentato per la mia class TypeSwitch . Questa è una versione abbreviata di lavoro:

 public static class TypeSwitch { public static Switch On(TSource value) { return new Switch(value); } public sealed class Switch { private readonly TSource value; private bool handled = false; internal Switch(TSource value) { this.value = value; } public Switch Case(Action action) where TTarget : TSource { if (!this.handled && this.value is TTarget) { action((TTarget) this.value); this.handled = true; } return this; } public void Default(Action action) { if (!this.handled) action(this.value); } } } 

Crea una superclass (S) e fai ereditare A e B da essa. Quindi dichiarare un metodo astratto su S che ogni sottoclass deve implementare.

Facendo questo il metodo “foo” può anche cambiare la sua firma in Foo (S o), rendendolo sicuro, e non è necessario lanciare quell’eccezionale eccezione.

Se stavi usando C # 4, potresti sfruttare la nuova funzionalità dynamic per ottenere un’alternativa interessante. Non sto dicendo che è meglio, anzi, sembra molto probabile che sia 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 questo funziona è che una chiamata al metodo dinamico C # 4 ha i suoi overload risolti in fase di esecuzione piuttosto che in fase di compilazione. Ho scritto un po ‘di più su questa idea abbastanza recentemente . Ancora una volta, vorrei solo ribadire che questo probabilmente si comporta peggio di tutti gli altri suggerimenti, lo sto offrendo semplicemente come una curiosità.

Dovresti davvero sovraccaricare il tuo metodo, non provare a fare da solo la disambiguazione. La maggior parte delle risposte finora non prende in considerazione sottoclassi future, il che potrebbe portare a problemi di manutenzione davvero terribili in seguito.

Mi è piaciuto l’ uso di Virtlink della digitazione implicita per rendere il passaggio molto più leggibile, ma non mi piaceva che un early-out non fosse ansible e che stessimo facendo assegnazioni. Alziamo un po ‘il perf.

 public static class TypeSwitch { public static void On(TV value, Action action1) where T1 : TV { if (value is T1) action1((T1)value); } public static void On(TV value, Action action1, Action 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 value, Action action1, Action action2, Action 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, questo mi fa male alle 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 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 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#> } 

Adeguare 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 continuano a sottolineare nelle loro risposte, e data la natura di questa domanda, l’ordine è importante nella corrispondenza del tipo. Perciò:

  • Metti prima i tipi di foglie, poi i tipi di base.
  • Per i tipi di pari, mettere prima le corrispondenze più probabili per massimizzare la perf.
  • Ciò implica che non è necessario un caso predefinito speciale. Invece, usa semplicemente il tipo base-più nel lambda, e mettilo per ultimo.

Per i tipi predefiniti, è ansible utilizzare l’enumerazione TypeCode. Si noti che GetType () è un po ‘lento, ma probabilmente non rilevante 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, puoi creare la tua enumerazione e un’interfaccia o una class base con proprietà o metodi astratti …

Implementazione di class 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 di class astratta del metodo

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

Uno dei miei colleghi mi ha appena parlato anche di questo: questo ha il vantaggio che puoi usarlo letteralmente per qualsiasi tipo di object, non solo per quelli che definisci. Ha lo svantaggio di essere un po ‘più grande e più lento.

Per prima cosa definisci una class 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 typeDict; static TypeEnumerator() { typeDict = new Dictionary(); typeDict[typeof(int)] = TypeEnumeratorTypes._int; typeDict[typeof(string)] = TypeEnumeratorTypes._string; typeDict[typeof(Foo)] = TypeEnumeratorTypes._Foo; typeDict[typeof(System.Net.Sockets.TcpClient)] = TypeEnumeratorTypes._TcpClient; } ///  /// Throws NullReferenceException and TypeEnumeratorException /// NullReferenceException /// TypeEnumeratorException 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; } 

Dato che l’ereditarietà consente a un object di essere riconosciuto come più di un tipo, penso che un cambiamento possa portare a una ctriggers 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 object. Penso che quando scrivi un switch(foo) ti aspetti che foo corrisponda a una e solo una delle affermazioni del case . Con un tipo di triggerszione, l’ordine in cui si scrivono le dichiarazioni del caso potrebbe modificare il risultato dell’intera dichiarazione dell’interruttore. Penso che sarebbe sbagliato.

Si potrebbe pensare a un compilatore-verificare i tipi di un’istruzione “tipo”, controllando che i tipi enumerati non ereditino l’uno dall’altro. Questo però non esiste.

foo is T non è la stessa di foo.GetType() == typeof(T) !!

Lo farei anch’io

  • usa il metodo overloading (proprio come x0n ), o
  • utilizzare sottoclassi (proprio come Pablo ), o
  • applica il modello di visitatore .

Ho esaminato alcune opzioni qui, rispecchiando ciò che F # può fare. F # ha un supporto molto migliore per il passaggio basato sul tipo (anche se sto ancora attaccando a C #; –p). Potresti voler vedere qui e qui .

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

Sì, grazie a C # 7 questo può essere raggiunto, ecco come è fatto (usando il pattern di espressione ):

  switch(o) { case A a: a.Hop(); break; case B b: b.Skip(); break; case C _: return new ArgumentException("Type C will be supported in the next version"); default: return new ArgumentException("Unexpected type: " + o.GetType()); } 

Crea un’interfaccia IFooable, quindi rendi le tue classi A e B 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 “come”, invece prima controllare con “è” e quindi eseguire il casting, in questo modo si fanno 2 calchi (costosi).

Puoi creare metodi sovraccaricati:

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

E usa il tipo di parametro dinamico per aggirare il controllo di tipo statico:

 Foo((dynamic)something); 

Stai cercando le Discriminated Unions che sono una funzionalità linguistica di F #, ma puoi ottenere un effetto simile usando una libreria che ho creato, chiamata OneOf

https://github.com/mcintyre321/OneOf

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

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

Se si aggiunge un terzo elemento a o, si otterrà un errore del compilatore in quanto è necessario aggiungere un gestore Func all’interno della chiamata switch.

Puoi anche fare un .Match che restituisce un valore, piuttosto che eseguire una dichiarazione:

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

Sono d’accordo con Jon sul fatto di avere un hash di azioni in nome della class. Se mantieni il tuo pattern, potresti prendere in considerazione l’ipotesi di utilizzare il costrutto “as”:

 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 il patter se (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 conterei.

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

Anche questo è implementato in BCL. Un esempio è MemberInfo.MemberTypes , un altro è GetTypeCode per 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 ...... } } 

Questa è una risposta alternativa che unisce 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.
  • Garantisce che sia stato costruito correttamente e che esista sempre una funzione predefinita .
  • Ritorna dopo la prima corrispondenza (vero per JaredPar answer, non vero per VirtLink one).

Uso:

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

Codice:

 public class TSwitch { class CaseInfo { public Type Target { get; set; } public Func Func { get; set; } } private object _source; private List> _cases; public static TSwitch On(object source) { return new TSwitch { _source = source, _cases = new List>() }; } public TResult Default(Func defaultFunc) { var srcType = _source.GetType(); foreach (var entry in _cases) if (entry.Target.IsAssignableFrom(srcType)) return entry.Func(_source); return defaultFunc(_source); } public TSwitch Case(Func func) { _cases.Add(new CaseInfo { Func = x => func((TSource)x), Target = typeof(TSource) }); return this; } }