Czy istnieje lepsza alternatywa dla „włączania typu”?

331

Widząc, że C # nie może być switchna typie (który, jak uznaję, nie został dodany jako szczególny przypadek, ponieważ isrelacje oznaczają, że casemoże mieć zastosowanie więcej niż jedna odmiana), czy jest lepszy sposób na symulację włączenia typu innego niż ten?

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());
    }
}
xyz
źródło
18
Z ciekawości, dlaczego po prostu nie używasz polimorfizmu?
18
@jeyoung zapieczętowane klasy i nie jest tego warte w sytuacjach ad-hoc
xyz
2
@jeyoung: Jedną z typowych sytuacji, w których nie można zastosować polimorfizmu, jest to, że przełączane typy nie mogą znać kodu zawierającego switchinstrukcję. Jeden przykład: zestaw A zawiera zestaw obiektów danych (które nie zostaną zmienione, zdefiniowane w dokumencie specyfikacji lub tym podobne). Zespoły B , C i D każde odniesienie i dostarczenie konwersji dla różnych obiektów danych z A (np serializacji / deserializacji pewnym określonym formacie). Musisz albo odzwierciedlić całą hierarchię klas w B , C i D i korzystać z fabryk, albo masz ...
LUB Mapper

Odpowiedzi:

276

W C # zdecydowanie brakuje przełączania typów ( AKTUALIZACJA: w C # 7 / VS 2017 włączanie typów jest obsługiwane - patrz odpowiedź Zachary'ego Yatesa poniżej ). Aby to zrobić bez dużej instrukcji if / else if / else, musisz pracować z inną strukturą. Niedługo napisałem post na blogu, w którym szczegółowo opisałem, jak zbudować strukturę TypeSwitch.

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

Wersja skrócona: TypeSwitch ma za zadanie zapobiegać redundantnym rzutowaniom i zapewniać składnię podobną do normalnej instrukcji switch / case. Na przykład tutaj jest TypeSwitch w akcji na standardowym zdarzeniu formularza Windows

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

Kod TypeSwitch jest w rzeczywistości dość mały i można go łatwo umieścić w projekcie.

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
        };
    }
}
JaredPar
źródło
26
„type == entry.Target” można również zmienić na „entry.Target.IsAssignableFrom (type)”, aby uwzględnić kompatybilne typy (np. podklasy).
Mark Cidade,
Zmieniono kod, aby używał „entry.Target.IsAssignableFrom (type)”, tak aby obsługiwane były podklasy.
Matt Howells,
3
Warto zwrócić uwagę na to, że (z tego co rozumiem) wymagane jest określenie działania „domyślnego” jako ostatniego, aby upewnić się, że wszystkie pozostałe przypadki są sprawdzone. Uważam, że nie jest to wymóg standardowego przełącznika - nie że nigdy nie widziałem, aby ktoś próbował ustawić „domyślną” gdziekolwiek poza dnem. Kilka opcji bezpiecznych w razie awarii może polegać na zamówieniu tablicy w celu upewnienia się, że wartość domyślna jest ostatnia (nieco marnotrawstwo) lub wstawieniu wartości domyślnej w zmiennej, która zostanie przetworzona po foreach(co by się kiedykolwiek zdarzyło, gdyby nie znaleziono dopasowania)
musefan
Co jeśli nadawca ma wartość zerową? GetType zgłosi wyjątek
Jon
Dwie sugestie: Obsługuj zerowe źródło, wywołując wartość domyślną lub zgłaszając wyjątek i pozbądź się wartości logicznej, po CaseInfoprostu sprawdzając wartość typu (jeśli jej wartość jest zerowa, to domyślna).
Felix K.,
291

W wersji C # 7 dostarczonej z programem Visual Studio 2017 (wydanie 15. *) możesz używać typów w caseinstrukcjach (dopasowywanie wzorca):

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

W C # 6 możesz użyć instrukcji switch z operatorem nameof () (dzięki @Joey Adams):

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

W wersji C # 5 i wcześniejszych możesz użyć instrukcji switch, ale będziesz musiał użyć magicznego ciągu zawierającego nazwę typu ... który nie jest szczególnie przyjazny dla refaktorów (dzięki @nukefusion)

switch(o.GetType().Name) {
  case "AType":
    break;
}
Zachary Yates
źródło
1
działa to z typem przypadku (string) .Name: ... lub musi być z Valuetype?
Tomer W
3
Zaciemnienie może to zepsuć
Konrad Morawski
6
@ nukefusion: To znaczy, chyba że użyjesz nowego błyszczącego nameof()operatora .
Joey Adams
21
Nie podoba mi się ta odpowiedź, ponieważ nameof (NamespaceA.ClassC) == nameof (NamespaceB.ClassC) jest prawdą.
ischas
7
(c # 7) możesz także użyć podkreślenia, jeśli nie potrzebujesz dostępu do obiektu:case UnauthorizedException _:
Assaf S.
101

Jedną z opcji jest posiadanie słownika od Typedo Action(lub innego delegata). Wyszukaj akcję na podstawie typu, a następnie wykonaj ją. Użyłem tego wcześniej do fabryk.

Jon Skeet
źródło
31
Drobna uwaga: dobra na mecze 1: 1, ale może być uciążliwa z dziedziczeniem i / lub interfejsami - zwłaszcza, że ​​nie ma gwarancji zachowania porządku w słowniku. Ale nadal tak robię w kilku miejscach ;-p Więc +1
Marc Gravell
@Marc: Jak spadłyby dziedzictwo lub interfejsy w tym paradygmacie? Zakładając, że klucz jest typem, a akcja jest metodą, to dziedziczenie lub interfejsy powinny w rzeczywistości wymuszać prawidłową rzecz (TM), o ile mi wiadomo. Z pewnością rozumiem problem z wieloma działaniami i brakiem zamówienia.
Harper Shelby,
2
W przeszłości korzystałem z tej techniki, zwykle przed przeprowadzką do kontenera IoC
Chris Canal,
4
Ta technika dzieli się na dziedziczenie i interfejsy, ponieważ potrzebujesz korespondencji jeden do jednego między obiektem, który sprawdzasz, a delegatem, do którego dzwonisz. Który z wielu interfejsów obiektu powinieneś spróbować znaleźć w słowniku?
Robert Rossney,
5
Jeśli budujesz słownik specjalnie do tego celu, możesz przeładować indeksator, aby zwrócić wartość typu klucza, lub jeśli go brakuje, to jego nadklasa, jeśli go brakuje, to ta nadklasa itp., Dopóki nic nie pozostanie.
Erik Forbes,
49

Z odpowiedzią JaredPara z tyłu głowy napisałem wariant jego TypeSwitchklasy, który używa wnioskowania typu dla lepszej składni:

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

Pamiętaj, że kolejność Case()metod jest ważna.


Uzyskaj pełny i skomentowany kod dla mojej TypeSwitchklasy . To jest działająca wersja skrócona:

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);
        }
    }
}
Daniel AA Pelsmaeker
źródło
Wygląda na dobre rozwiązanie i chciałem zobaczyć, co jeszcze możesz o tym powiedzieć, ale blog nie żyje.
Wes Grant
1
Cholera, masz rację. Mój host ma problemy od godziny. Pracują nad tym. Wpis na moim blogu jest zasadniczo taki sam, jak odpowiedź tutaj, ale zawiera link do pełnego kodu źródłowego.
Daniel AA Pelsmaeker
1
Uwielbiam, jak to redukuje kilka nawiasów if do prostego przełącznika „funkcjonalnego”. Dobra robota!
James White
2
Można również dodać metodę rozszerzenia do wstępnego postępowania: public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSource. To pozwala powiedziećvalue.Case((C x) ...
Joey Adams,
1
@JoeyAdams: Uwzględniłem twoją ostatnią sugestię oraz kilka drobnych usprawnień. Jednak utrzymuję tę samą składnię.
Daniel AA Pelsmaeker
14

Utwórz nadklasę (S) i odziedzicz po niej A i B. Następnie zadeklaruj abstrakcyjną metodę na S, którą każda podklasa musi zaimplementować.

Robiąc to, metoda „foo” może również zmienić swój podpis na Foo (S o), dzięki czemu jest bezpieczny i nie musisz rzucać tego brzydkiego wyjątku.

Pablo Fernandez
źródło
Prawdziwy Bruno, ale pytanie tego nie sugeruje. Możesz to uwzględnić w swojej odpowiedzi, choć Pablo.
Dana the Sane
Z pytania wynika, że ​​A i B są na tyle ogólne, że mogą być A = String; B = Lista <int> na przykład ...
bruno conde
13

Możesz użyć dopasowania wzorca w C # 7 lub nowszej:

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;
}
alhpe
źródło
Dziękuję za to! Może być również używany do wykrywania podklas: if (this.TemplatedParent.GetType (). IsSubclassOf (typeof (RadGridView))) można zmienić na: switch (this.TemplatedParent.GetType ()) case var subRadGridView when subRadGridView.IsSubclassOf ( typeof (RadGridView)):
Flemming Bonde Kentved,
Robisz to źle. Zobacz odpowiedź Serge'a Interna i przeczytaj o zasadzie podstawienia
Liskowa
8

Naprawdę powinieneś przeciążać swoją metodę, nie próbując samemu ujednoznacznić. Większość dotychczasowych odpowiedzi nie uwzględnia przyszłych podklas, co może później prowadzić do naprawdę strasznych problemów z obsługą.

sep332
źródło
3
Rozdzielczość przeciążenia jest określana statycznie, więc po prostu nie będzie działać.
Neutrino,
@ Neutrino: w pytaniu nie ma nic, co sugerowałoby, że ten typ nie jest znany w czasie kompilacji. A jeśli tak, to przeciążenie ma o wiele większy sens niż jakakolwiek inna opcja, biorąc pod uwagę oryginalny kod OP.
Peter Duniho
Myślę, że fakt, że próbuje on użyć instrukcji „if” lub „switch” do określenia typu, jest dość wyraźnym wskazaniem, że typ nie jest znany w czasie kompilacji.
Neutrino
@Neutrino, pamiętam, że jak zauważył Sergey Berezovskiy, w C # istnieje słowo kluczowe dynamiczne, które reprezentuje typ, który należy dynamicznie rozwiązać (w czasie wykonywania, a nie w czasie kompilacji).
Davide Cannizzo
8

Jeśli korzystasz z C # 4, możesz skorzystać z nowej dynamicznej funkcjonalności, aby uzyskać interesującą alternatywę. Nie twierdzę, że tak jest lepiej, w rzeczywistości wydaje się bardzo prawdopodobne, że będzie wolniejszy, ale ma w sobie pewną elegancję.

class Thing
{

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

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

}

A użycie:

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

Powodem tego jest fakt, że wywołanie metody dynamicznej w języku C # 4 zostało rozwiązane w czasie wykonywania zamiast w czasie kompilacji. Niedawno napisałem nieco więcej o tym pomyśle . Ponownie chciałbym tylko powtórzyć, że to prawdopodobnie działa gorzej niż wszystkie inne sugestie, oferuję to po prostu jako ciekawość.

Paul Batum
źródło
1
Miałem dzisiaj ten sam pomysł. Jest to około 3 razy wolniejsze niż włączenie nazwy typu. Oczywiście wolniejszy jest względny (dla 60 000 000 połączeń, tylko 4 sekundy), a kod jest o wiele bardziej czytelny, więc warto.
Daryl
8

Tak, dzięki C # 7, który można osiągnąć. Oto jak to się robi (używając wzorca wyrażenia ):

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());
}
Serge Intern
źródło
1
+1, patrz Dokumentacja funkcji C # 7
Florian Koch
7

W przypadku typów wbudowanych można użyć wyliczenia TypeCode. Należy pamiętać, że GetType () jest dość powolny, ale prawdopodobnie nie jest istotny w większości sytuacji.

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

Dla niestandardowych typów możesz utworzyć własne wyliczenie oraz interfejs lub klasę podstawową z abstrakcyjną właściwością lub metodą ...

Implementacja własności klasy abstrakcyjnej

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

Implementacja metody klasy abstrakcyjnej

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

Implementacja interfejsu własności

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

Implementacja metody interfejsu

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

Jeden z moich współpracowników właśnie mi o tym powiedział: ma tę zaletę, że można go używać do dosłownie każdego rodzaju obiektu, nie tylko tych, które zdefiniujesz. Ma tę wadę, że jest nieco większy i wolniejszy.

Najpierw zdefiniuj klasę statyczną w następujący sposób:

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

A potem możesz użyć tego w następujący sposób:

switch (TypeEnumerator.EnumerateType(someObject))
{
    case TypeEnumerator.TypeEnumeratorTypes._int:
        break;
    case TypeEnumerator.TypeEnumeratorTypes._string:
        break;
}
Edward Ned Harvey
źródło
Dzięki za dodanie TypeCode () - wariantu dla typów pierwotnych, ponieważ nawet wariant C # 7.0 - nie działa z nimi (oczywiście też nameof ())
Ole Albers
6

Podobało mi się użycie przez Virtlink pisania niejawnego, aby uczynić przełącznik znacznie bardziej czytelnym, ale nie podobało mi się, że wczesne wyjście nie jest możliwe i że dokonujemy alokacji. Podkręćmy trochę 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.
}

Cóż, to mnie boli. Zróbmy to w 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#>
}

Trochę zmieniając przykład 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());

Czytelny i szybki. Teraz, jak wszyscy wskazują w swoich odpowiedziach i biorąc pod uwagę naturę tego pytania, kolejność jest ważna w dopasowywaniu typów. W związku z tym:

  • Najpierw umieść typy liści, później typy podstawowe.
  • W przypadku typów równorzędnych najpierw stawiaj bardziej prawdopodobne dopasowania, aby zmaksymalizować perf.
  • Oznacza to, że nie ma potrzeby specjalnego domyślnego przypadku. Zamiast tego po prostu użyj najbardziej bazowego typu w lambda i umieść go na końcu.
scobi
źródło
5

Biorąc pod uwagę, że dziedziczenie ułatwia rozpoznanie obiektu jako więcej niż jednego typu, myślę, że zmiana może prowadzić do złej dwuznaczności. Na przykład:

Przypadek 1

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

Przypadek 2

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

Ponieważ s jest ciągiem i obiektem. Myślę, że kiedy piszesz switch(foo), oczekujesz, że foo będzie pasowało do jednego i tylko jednegocase stwierdzeń. Po włączeniu typów kolejność pisania instrukcji case może zmienić wynik całej instrukcji switch. Myślę, że to byłoby złe.

Można pomyśleć o sprawdzeniu kompilatora typów instrukcji „typwitch”, sprawdzeniu, czy wyliczone typy nie dziedziczą po sobie. To jednak nie istnieje.

foo is Tto nie to samo co foo.GetType() == typeof(T)!!

Evren Kuzucuoglu
źródło
4

Ja też bym

  • użyj metody przeciążenia (podobnie jak x0n ) lub
  • użyj podklas (tak jak Pablo ) lub
  • zastosuj wzór gościa .
Jonas Kongslund
źródło
4

Innym sposobem byłoby zdefiniowanie interfejsu IThing, a następnie zaimplementowanie go w obu klasach, oto fragment kodu:

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();
    }
}
jgarcia
źródło
4

Jak na C # 7.0 specyfikacji, można zadeklarować zmienną lokalną scoped w sposób casetematyce 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;
}

Jest to najlepszy sposób na zrobienie czegoś takiego, ponieważ obejmuje po prostu rzutowanie i operacje push-on-the-stack, które są najszybszymi operacjami, jakie interpreter może wykonać tuż po bitowych operacjach i booleanwarunkach.

Porównując to do Dictionary<K, V>, tutaj jest o wiele mniejsze zużycie pamięci: trzymanie słownika wymaga więcej miejsca w pamięci RAM i trochę więcej obliczeń przez procesor do utworzenia dwóch tablic (jednej dla kluczy, a drugiej dla wartości) i zebrania kodów mieszających dla kluczy do umieszczenia wartości do odpowiednich kluczy.

Tak więc, o ile wiem, nie sądzę, aby istniał szybszy sposób, chyba że chcesz użyć tylko if- then- elsebloku z isoperatorem w następujący sposób:

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.
Davide Cannizzo
źródło
3

Możesz tworzyć przeciążone metody:

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

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

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

I rzuć argument, aby dynamicwpisać, aby ominąć sprawdzanie typu statycznego:

Foo((dynamic)something);
Siergiej Berezowski
źródło
3

Udoskonalenia dopasowania wzoru w języku C # 8 umożliwiły to w ten sposób. W niektórych przypadkach spełnia swoje zadanie i jest bardziej zwięzły.

        public Animal Animal { get; set; }
        ...
        var animalName = Animal switch
        {
            Cat cat => "Tom",
            Mouse mouse => "Jerry",
            _ => "unknown"
        };
PilgrimViis
źródło
2

Szukasz Discriminated Unionsfunkcji języka F #, ale możesz osiągnąć podobny efekt, korzystając z utworzonej przeze mnie biblioteki OneOf

https://github.com/mcintyre321/OneOf

Główną zaletą w stosunku do switch(a if, a exceptions as control flow) jest to, że podczas kompilacji bezpieczny - nie ma domyślnego obsługi lub nie mieści się

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

Jeśli dodasz trzeci element do o, pojawi się błąd kompilatora, ponieważ musisz dodać funkcję obsługi Func w wywołaniu przełącznika.

Możesz także wykonać polecenie, .Matchktóre zwraca wartość, zamiast wykonać instrukcję:

double Area(OneOf<Square, Circle> o)
{
    return o.Match(
        square => square.Length * square.Length,
        circle => Math.PI * circle.Radius * circle.Radius
    );
}
mcintyre321
źródło
2

Utwórz interfejs IFooable, a następnie spraw , aby twoja Ai Bklasy zaimplementowały wspólną metodę, która z kolei wywołuje odpowiednią metodę, którą chcesz:

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

Zauważ, że lepiej jest użyć aszamiast tego najpierw sprawdzania za pomocą, isa następnie rzucania, ponieważ w ten sposób wykonujesz 2 rzuty, więc jest to droższe.

Sunny Milenov
źródło
2

W takich przypadkach zwykle kończę na liście predykatów i działań. Coś w tym stylu:

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);
    }
}
Hallgrim
źródło
2

Po porównaniu opcji przedstawionych tutaj w odpowiedzi na kilka funkcji F #, odkryłem, że F # ma znacznie lepszą obsługę przełączania opartego na typach (chociaż nadal trzymam się C #).
Możesz chcieć zobaczyć tu i tutaj .

Marc Gravell
źródło
2
<włóż wtyczkę do F # tutaj>
Overlord Zurg
1

Stworzyłbym interfejs z dowolną nazwą i nazwą metody, która miałaby sens dla twojego przełącznika, nazwijmy je odpowiednio: IDoableto mówi o implementacji 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();
    }
}

i zmień metodę w następujący sposób:

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

Przynajmniej jesteś bezpieczny w czasie kompilacji i podejrzewam, że pod względem wydajności jest to lepsze niż sprawdzanie typu w czasie wykonywania.

Kerry Perret
źródło
1

Od C # 8 możesz sprawić, że będzie jeszcze bardziej zwięzły dzięki nowemu przełącznikowi. Za pomocą opcji discard _ można uniknąć tworzenia niepotrzebnych zmiennych, gdy nie są potrzebne, na przykład:

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

Faktura i lista wysyłkowa to klasy, a dokument to obiekt, którym może być dowolna z nich.

David
źródło
0

Zgadzam się z Jonem w kwestii posiadania skrótu akcji do nazwy klasy. Jeśli utrzymasz swój wzorzec, możesz zamiast tego rozważyć użycie konstrukcji „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("...");

Różnica polega na tym, że gdy używasz tupotu, jeśli (foo to Bar) {((Bar) foo) .Action (); } wykonujesz rzut typu dwa razy. Teraz może kompilator zoptymalizuje i wykona tę pracę tylko raz - ale nie liczyłbym na to.

cokół
źródło
1
Naprawdę nie lubię wielu punktów wyjścia (powrotów), ale jeśli chcesz się tego trzymać, dodaj na początku „if (o == null) throw”, ponieważ później nie będziesz wiedział, czy rzut nie powiódł się, czy obiekt był pusty.
Sunny Milenov,
0

Jak sugeruje Pablo, podejście do interfejsu jest prawie zawsze właściwe, aby sobie z tym poradzić. Aby naprawdę skorzystać z przełącznika, inną alternatywą jest posiadanie niestandardowego wyliczenia oznaczającego twój typ na zajęciach.

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

Jest to również zaimplementowane w BCL. Jednym z przykładów jest MemberInfo.MemberTypes , innym jest GetTypeCodedla typów pierwotnych, takich jak:

void Foo(object o)
{
    switch (Type.GetTypeCode(o.GetType())) // for IConvertible, just o.GetTypeCode()
    {
        case TypeCode.Int16:
        case TypeCode.Int32:
        //etc ......
    }
}
nawfal
źródło
0

To jest alternatywna odpowiedź, która łączy wkłady z odpowiedzi JaredPar i VirtLink, z następującymi ograniczeniami:

  • Konstrukcja przełącznika zachowuje się jak funkcja i odbiera funkcje jako parametry do spraw.
  • Zapewnia, że ​​jest poprawnie zbudowany i zawsze istnieje funkcja domyślna .
  • To wraca po pierwszym meczu (true dla JaredPar odpowiedź, nie jest prawdą dla VirtLink jeden).

Stosowanie:

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

Kod:

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;
    }
}
jruizaranguren
źródło
0

Tak - wystarczy użyć nieco dziwnie nazwanego „dopasowania wzorca” od C # 7 w górę, aby dopasować klasę lub strukturę:

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

switch (concrete1)
{
    case ObjectImplementation1 c1: return "type 1";         
    case ObjectImplementation2 c2: return "type 2";         
}
James Harcourt
źródło
0

używam

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

        if (t == typeof(CategoryDataStore))
            return (T)DependencyService.Get<IDataStore<ItemCategory>>();
        else
            return default(T);
    }
mdimai666
źródło
0

Powinien współpracować z

rodzaj sprawy _:

lubić:

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;
}
Jean-Maurice Destraz
źródło
0

Jeśli znasz klasę, której oczekujesz, ale nadal nie masz obiektu, możesz to zrobić:

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";
    }
}
Chan
źródło