Gibt es eine bessere Alternative zum Einschalten des Typs?

331

Gibt es eine bessere Möglichkeit, das Einschalten eines anderen Typs zu simulieren, da C # switchfür einen Typ nicht möglich ist (was meiner Ansicht nach nicht als Sonderfall hinzugefügt wurde, da isBeziehungen bedeuten, dass casemöglicherweise mehr als ein eindeutiger Typ gilt)?

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
quelle
18
Warum verwenden Sie aus Neugier nicht einfach Polymorphismus?
18
@jeyoung versiegelte Klassen, und es lohnt sich nicht für Ad-hoc-Situationen
xyz
2
@jeyoung: Eine typische Situation, in der Polymorphismus nicht verwendet werden kann, besteht darin, dass die Typen, die umgeschaltet werden, den Code, der die switchAnweisung enthält, nicht kennen dürfen . Ein Beispiel: Baugruppe A enthält eine Reihe von Datenobjekten (die sich nicht ändern werden, definiert in einem Spezifikationsdokument oder dergleichen). Die Baugruppen B , C und D verweisen jeweils auf A und stellen eine Konvertierung für die verschiedenen Datenobjekte von A bereit (z. B. eine Serialisierung / Deserialisierung in ein bestimmtes Format). Sie müssen entweder die gesamte Klassenhierarchie in B , C und D spiegeln und Fabriken verwenden, oder Sie haben ...
ODER Mapper

Antworten:

276

Das Einschalten von Typen fehlt in C # definitiv ( UPDATE: In C # 7 / VS 2017 wird das Einschalten von Typen unterstützt - siehe Zachary Yates 'Antwort unten ). Um dies ohne eine große if / else if / else-Anweisung zu tun, müssen Sie mit einer anderen Struktur arbeiten. Ich habe vor einiger Zeit einen Blog-Beitrag geschrieben, in dem beschrieben wird, wie eine TypeSwitch-Struktur erstellt wird.

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

Kurzversion: TypeSwitch wurde entwickelt, um redundantes Casting zu verhindern und eine Syntax anzugeben, die einer normalen switch / case-Anweisung ähnelt. Hier ist beispielsweise TypeSwitch in Aktion für ein Standard-Windows-Formularereignis

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

Der Code für TypeSwitch ist eigentlich ziemlich klein und kann einfach in Ihr Projekt eingefügt werden.

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
quelle
26
"type == entry.Target" kann auch in "entry.Target.IsAssignableFrom (type)" geändert werden, um kompatible Typen (z. B. Unterklassen) zu berücksichtigen.
Mark Cidade
Der Code wurde so geändert, dass "entry.Target.IsAssignableFrom (type)" verwendet wird, sodass Unterklassen unterstützt werden.
Matt Howells
3
Eine Sache, die vielleicht erwähnenswert ist, ist, dass (soweit ich weiß) die letzte Standardaktion angegeben werden muss, um sicherzustellen, dass alle anderen Fälle überprüft werden. Ich glaube, dass dies bei einem Standardschalter keine Voraussetzung ist - nicht, dass ich jemals jemanden gesehen habe, der versucht hat, irgendwo anders als unten einen "Standard" zu setzen. Ein paar fail safe Optionen dafür könnte sein , um das Array zu bestellen Standard letzte ist (Bit verschwenderisch) oder Pop den Standard in einer Variablen , um sicherzustellen , wird verarbeitet , nachdem die foreach(nicht gefunden wurde , die nur jemals passieren würde , wenn ein Spiel)
Musefan
Was ist, wenn der Absender null ist? GetType wird eine Ausnahme auslösen
Jon
Zwei Vorschläge: Behandeln Sie die Nullquelle, indem Sie default aufrufen oder eine Ausnahme auslösen, und entfernen Sie den Booleschen CaseInfoWert in, indem Sie nur den Typwert überprüfen (wenn er null ist, ist dies der Standardwert).
Felix K.
290

Mit C # 7 , das mit Visual Studio 2017 (Release 15. *) ausgeliefert wurde, können Sie Typen in caseAnweisungen verwenden (Mustervergleich):

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

Mit C # 6 können Sie eine switch-Anweisung mit dem Operator nameof () verwenden (danke @Joey Adams):

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

Mit C # 5 und früheren Versionen könnten Sie eine switch-Anweisung verwenden, aber Sie müssen eine magische Zeichenfolge verwenden, die den Typnamen enthält ... was nicht besonders refaktorfreundlich ist (danke @nukefusion).

switch(o.GetType().Name) {
  case "AType":
    break;
}
Zachary Yates
quelle
1
funktioniert das mit case typeof (string) .Name: ... oder muss es mit Valuetype sein?
Tomer W
3
Verschleierung kann es brechen
Konrad Morawski
6
@nukefusion: Das heißt, es sei denn, Sie verwenden den glänzenden neuen nameof()Operator .
Joey Adams
21
Diese Antwort gefällt mir nicht, weil nameof (NamespaceA.ClassC) == nameof (NamespaceB.ClassC) wahr ist.
Ischas
7
(c # 7) Sie können auch Unterstrich verwenden, wenn Sie keinen Zugriff auf das Objekt benötigen:case UnauthorizedException _:
Assaf S.
101

Eine Möglichkeit besteht darin, ein Wörterbuch von Typebis Action(oder einen anderen Delegaten) zu haben. Suchen Sie die Aktion anhand des Typs und führen Sie sie dann aus. Ich habe das schon früher für Fabriken benutzt.

Jon Skeet
quelle
31
Kleinere Anmerkung: Gut für 1: 1-Übereinstimmungen, kann jedoch zu Problemen bei der Vererbung und / oder den Schnittstellen führen - insbesondere, da die Reihenfolge mit einem Wörterbuch nicht garantiert erhalten bleibt. Trotzdem ist es so, wie ich es an einigen Stellen mache ;-p Also +1
Marc Gravell
@Marc: Wie würden Vererbung oder Schnittstellen in diesem Paradigma brechen? Angenommen, der Schlüssel ist ein Typ und die Aktion ist eine Methode, dann sollten Vererbung oder Schnittstellen, soweit ich das beurteilen kann, tatsächlich das Richtige erzwingen. Ich verstehe das Problem mit mehreren Aktionen und mangelnder Reihenfolge.
Harper Shelby
2
Ich habe diese Technik in der Vergangenheit viel angewendet, normalerweise bevor ich
Chris Canal
4
Diese Technik wird für Vererbung und Schnittstellen unterbrochen, da Sie eine Eins-zu-Eins-Entsprechung zwischen dem zu überprüfenden Objekt und dem von Ihnen angerufenen Delegaten benötigen. Welche der mehreren Schnittstellen eines Objekts sollten Sie im Wörterbuch suchen?
Robert Rossney
5
Wenn Sie ein Wörterbuch speziell für diesen Zweck erstellen, können Sie den Indexer überladen, um den Wert des Schlüsseltyps zurückzugeben, oder wenn er fehlt, dann seine Oberklasse, wenn dieser fehlt, dann diese Oberklasse usw., bis nichts mehr übrig ist.
Erik Forbes
49

Mit JaredPars Antwort im Hinterkopf schrieb ich eine Variante seiner TypeSwitchKlasse, die Typinferenz für eine schönere Syntax verwendet:

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

Beachten Sie, dass die Reihenfolge der Case()Methoden wichtig ist.


Holen Sie sich den vollständigen und kommentierten Code für meine TypeSwitchKlasse . Dies ist eine funktionierende Kurzversion:

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
quelle
Sieht nach einer guten Lösung aus und wollte sehen, was Sie sonst noch dazu zu sagen haben, aber der Blog ist tot.
Wes Grant
1
Verdammt, du hast recht. Mein Webhost hat seit einer Stunde einige Probleme. Sie arbeiten daran. Der Beitrag in meinem Blog entspricht im Wesentlichen der Antwort hier, enthält jedoch einen Link zum vollständigen Quellcode.
Daniel AA Pelsmaeker
1
Ich liebe es, wie dies eine Reihe von if-Klammern auf einen einfachen "funktionalen" Schalter reduziert. Gute Arbeit!
James White
2
Sie können auch eine Erweiterungsmethode für den Anfangsfall hinzufügen : public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSource. Dies lässt Sie sagenvalue.Case((C x) ...
Joey Adams
1
@JoeyAdams: Ich habe Ihren letzten Vorschlag zusammen mit einigen kleinen Verbesserungen aufgenommen. Ich behalte jedoch die Syntax bei.
Daniel AA Pelsmaeker
14

Erstellen Sie eine Oberklasse (S) und lassen Sie A und B davon erben. Deklarieren Sie dann eine abstrakte Methode für S, die jede Unterklasse implementieren muss.

Wenn Sie dies tun, kann die "foo" -Methode auch ihre Signatur in Foo (S o) ändern, wodurch sie typsicher wird, und Sie müssen diese hässliche Ausnahme nicht auslösen.

Pablo Fernandez
quelle
Echter Bruno, aber die Frage legt das nicht nahe. Sie könnten das in Ihre Antwort über Pablo aufnehmen.
Dana the Sane
Aus der Frage denke ich, dass A und B generisch genug sind, dass sie A = String sein können; B = Liste <int> zum Beispiel ...
bruno conde
13

Sie können den Mustervergleich in C # 7 oder höher verwenden:

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
quelle
Danke für diesen! Kann auch zum Erkennen von Unterklassen verwendet werden: if (this.TemplatedParent.GetType (). IsSubclassOf (typeof (RadGridView))) kann geändert werden in: switch (this.TemplatedParent.GetType ()) case var subRadGridView when subRadGridView.IsSubclassOf ( typeof (RadGridView)):
Flemming Bonde Kentved
Du machst das falsch. Lesen Sie die Antwort von Serge Intern und lesen Sie das Liskov-Substitutionsprinzip
0xF
8

Sie sollten Ihre Methode wirklich überladen und nicht versuchen, die Begriffsklärung selbst vorzunehmen. Die meisten Antworten berücksichtigen bisher keine zukünftigen Unterklassen, was später zu wirklich schrecklichen Wartungsproblemen führen kann.

sep332
quelle
3
Die Überlastungsauflösung wird statisch bestimmt, sodass sie überhaupt nicht funktioniert.
Neutrino
@Neutrino: Es gibt nichts in der Frage, was vorschreibt, dass der Typ zur Kompilierungszeit nicht bekannt ist. Und wenn ja, ist eine Überlastung angesichts des ursprünglichen Codebeispiels des OP weitaus sinnvoller als jede andere Option.
Peter Duniho
Ich denke, die Tatsache, dass er versucht, eine 'if'- oder' switch'-Anweisung zu verwenden, um den Typ zu bestimmen, ist ein ziemlich klarer Hinweis darauf, dass der Typ zur Kompilierungszeit nicht bekannt ist.
Neutrino
@Neutrino, ich erinnere mich an Sie, dass es, wie Sergey Berezovskiy betonte, das dynamische Schlüsselwort in C # gibt, das einen Typ darstellt, der dynamisch gelöst werden muss (zur Laufzeit und nicht zur Kompilierungszeit).
Davide Cannizzo
8

Wenn Sie C # 4 verwenden, können Sie die neue dynamische Funktionalität nutzen, um eine interessante Alternative zu erzielen. Ich sage nicht, dass dies besser ist, tatsächlich scheint es sehr wahrscheinlich, dass es langsamer sein würde, aber es hat eine gewisse Eleganz.

class Thing
{

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

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

}

Und die Verwendung:

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

Der Grund dafür ist, dass bei einem dynamischen C # 4-Methodenaufruf die Überladungen zur Laufzeit und nicht zur Kompilierungszeit behoben werden. Ich habe vor kurzem etwas mehr über diese Idee geschrieben . Ich möchte noch einmal wiederholen, dass dies wahrscheinlich schlechter abschneidet als alle anderen Vorschläge. Ich biete es einfach als Kuriosität an.

Paul Batum
quelle
1
Ich hatte heute die gleiche Idee. Es ist ungefähr dreimal langsamer als das Einschalten des Typnamens. Natürlich ist langsamer relativ (für 60.000.000 Anrufe nur 4 Sekunden), und der Code ist so viel besser lesbar, dass es sich lohnt.
Daryl
8

Ja, dank C # 7 kann dies erreicht werden. So geht's (mit Ausdrucksmuster ):

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
quelle
1
+1, siehe Dokumentation für C # 7-Funktionen
Florian Koch
7

Für integrierte Typen können Sie die TypeCode-Enumeration verwenden. Bitte beachten Sie, dass GetType () etwas langsam ist, aber in den meisten Situationen wahrscheinlich nicht relevant.

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

Für benutzerdefinierte Typen können Sie eine eigene Aufzählung erstellen und entweder eine Schnittstelle oder eine Basisklasse mit abstrakter Eigenschaft oder Methode ...

Abstrakte Klassenimplementierung von Eigentum

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

Abstrakte Klassenimplementierung der Methode

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

Schnittstellenimplementierung von Eigenschaften

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

Schnittstellenimplementierung der Methode

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

Einer meiner Mitarbeiter hat mir auch gerade davon erzählt: Dies hat den Vorteil, dass Sie es für buchstäblich jede Art von Objekt verwenden können, nicht nur für diejenigen, die Sie definieren. Es hat den Nachteil, etwas größer und langsamer zu sein.

Definieren Sie zunächst eine statische Klasse wie folgt:

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

Und dann können Sie es so verwenden:

switch (TypeEnumerator.EnumerateType(someObject))
{
    case TypeEnumerator.TypeEnumeratorTypes._int:
        break;
    case TypeEnumerator.TypeEnumeratorTypes._string:
        break;
}
Edward Ned Harvey
quelle
Vielen Dank für das Hinzufügen der TypeCode () - Variante für primitive Typen, da selbst die C # 7.0 - Variante mit diesen nicht funktioniert (und nameof () offensichtlich auch nicht)
Ole Albers
6

Ich mochte Virtlinks Verwendung der impliziten Eingabe , um den Schalter besser lesbar zu machen, aber ich mochte nicht, dass ein Early-Out nicht möglich ist und dass wir Zuweisungen vornehmen. Lassen Sie uns den Perf ein wenig aufdrehen.

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

Nun, das macht meine Finger weh. Lass es uns in T4 machen:

<#@ 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#>
}

Das Beispiel von Virtlink ein wenig anpassen:

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

Lesbar und schnell. Nun, wie jeder in seinen Antworten immer wieder betont und die Art dieser Frage berücksichtigt, ist die Reihenfolge bei der Typübereinstimmung wichtig. Deshalb:

  • Stellen Sie die Blatttypen zuerst und die Basistypen später.
  • Stellen Sie bei Peer-Typen wahrscheinlichere Übereinstimmungen an die erste Stelle, um die Leistung zu maximieren.
  • Dies bedeutet, dass kein spezieller Standardfall erforderlich ist. Verwenden Sie stattdessen einfach den Basistyp im Lambda und setzen Sie ihn zuletzt ein.
scobi
quelle
5

Angesichts der Tatsache, dass die Vererbung es ermöglicht, ein Objekt als mehr als einen Typ zu erkennen, kann ein Wechsel meiner Meinung nach zu einer schlechten Mehrdeutigkeit führen. Zum Beispiel:

Fall 1

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

Fall 2

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

Weil s eine Zeichenfolge und ein Objekt ist. Ich denke, wenn du ein schreibst switch(foo), erwartest du, dass foo zu einem und nur einem der passtcase Aussagen . Bei einem Einschalttyp kann die Reihenfolge, in der Sie Ihre case-Anweisungen schreiben, möglicherweise das Ergebnis der gesamten switch-Anweisung ändern. Ich denke das wäre falsch.

Sie können sich eine Compilerprüfung der Typen einer "typeswitch" -Anweisung vorstellen, bei der überprüft wird, ob die aufgezählten Typen nicht voneinander erben. Das gibt es aber nicht.

foo is Tist nicht dasselbe wie foo.GetType() == typeof(T)!!

Evren Kuzucuoglu
quelle
4

Ich würde auch

  • Verwenden Sie die Methodenüberladung (genau wie x0n ) oder
  • Verwenden Sie Unterklassen (genau wie Pablo ) oder
  • Wenden Sie das Besuchermuster an .
Jonas Kongslund
quelle
4

Eine andere Möglichkeit wäre, ein Schnittstellen-IThing zu definieren und es dann in beiden Klassen zu implementieren. Hier ist das 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();
    }
}
jgarcia
quelle
4

Gemäß der C # 7.0-Spezifikation können Sie eine lokale Variable mit einem Gültigkeitsbereich casevon a deklarieren 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;
}

Dies ist der beste Weg, um so etwas zu tun, da es sich nur um Casting- und Push-on-the-Stack-Operationen handelt. Dies sind die schnellsten Operationen, die ein Interpreter unmittelbar nach bitweisen Operationen und booleanBedingungen ausführen kann.

Vergleichen Sie dies mit a Dictionary<K, V> ist hier viel weniger Speicher belegt: Das Halten eines Wörterbuchs erfordert mehr Speicherplatz im RAM und einige Berechnungen mehr von der CPU, um zwei Arrays (eines für Schlüssel und das andere für Werte) zu erstellen und Hash-Codes für die zu platzierenden Schlüssel zu sammeln Werte zu ihren jeweiligen Schlüsseln.

Soweit ich weiß, glaube ich nicht, dass es einen schnelleren Weg geben könnte, wenn Sie nicht nur einen if- then- elseBlock mit dem isOperator wie folgt verwenden möchten :

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
quelle
3

Sie können überladene Methoden erstellen:

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

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

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

Und wandeln Sie das Argument in dynamictype um, um die statische Typprüfung zu umgehen:

Foo((dynamic)something);
Sergey Berezovskiy
quelle
2

Sie suchen nach Discriminated Unionseiner Sprachfunktion von F #, aber Sie können einen ähnlichen Effekt erzielen, indem Sie eine von mir erstellte Bibliothek namens OneOf verwenden

https://github.com/mcintyre321/OneOf

Der Hauptvorteil gegenüber switch(und ifund exceptions as control flow) besteht darin, dass es zur Kompilierungszeit sicher ist - es gibt keinen Standardhandler oder Durchfall

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

Wenn Sie ein drittes Element zu o hinzufügen, wird ein Compilerfehler angezeigt, da Sie im Switch-Aufruf einen Handler Func hinzufügen müssen.

Sie können auch a .Matchausführen, das einen Wert zurückgibt, anstatt eine Anweisung auszuführen:

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

Erstellen Sie eine Schnittstelle IFooableund lassen Sie Ihre Aund BKlassen eine gemeinsame Methode implementieren, die wiederum die entsprechende Methode aufruft, die Sie möchten:

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

Beachten Sie, dass es besser ist, aszuerst zu prüfen isund dann zu wirken, da Sie auf diese Weise 2 Würfe machen, was teurer ist.

Sunny Milenov
quelle
2

In solchen Fällen erhalte ich normalerweise eine Liste mit Prädikaten und Aktionen. Etwas in diese Richtung:

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
quelle
2

Nachdem ich die Optionen verglichen hatte, die hier einige Antworten auf F # -Funktionen gegeben hatten, stellte ich fest, dass F # eine bessere Unterstützung für typbasiertes Umschalten bietet (obwohl ich mich immer noch an C # halte).
Vielleicht möchten Sie hier und hier sehen .

Marc Gravell
quelle
2
<Stecker für F # hier einstecken>
Overlord Zurg
2

C # 8-Verbesserungen des Mustervergleichs machten es möglich, dies so zu tun. In einigen Fällen erledigt es die Arbeit und prägnanter.

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

Ich würde eine Schnittstelle mit jedem Namen und Methodennamen erstellen, der für Ihren Switch sinnvoll wäre. Nennen wir sie jeweils: IDoabledas sagt, dass sie implementiert werden sollen 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();
    }
}

und ändern Sie die Methode wie folgt:

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

Zumindest sind Sie damit zur Kompilierungszeit sicher und ich vermute, dass es in Bezug auf die Leistung besser ist, als den Typ zur Laufzeit zu überprüfen.

Kerry Perret
quelle
0

Ich stimme Jon darin zu, dass es eine Reihe von Aktionen für den Klassennamen gibt. Wenn Sie Ihr Muster beibehalten, sollten Sie stattdessen das Konstrukt "as" verwenden:

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

Der Unterschied besteht darin, dass wenn Sie das Muster verwenden, wenn (foo ist Bar) {((Bar) foo) .Action (); } Du machst das Typ Casting zweimal. Jetzt wird der Compiler vielleicht optimieren und diese Arbeit nur einmal ausführen - aber ich würde mich nicht darauf verlassen.

Sockel
quelle
1
Ich mag wirklich nicht mehrere Austrittspunkte (Rückgaben), aber wenn Sie dabei bleiben möchten, fügen Sie am Anfang "if (o == null) throw" hinzu, da Sie später nicht wissen werden, ob die Besetzung nicht erfolgreich ist oder die Objekt war null.
Sunny Milenov
0

Wie Pablo vorschlägt, ist der Schnittstellenansatz fast immer das Richtige, um damit umzugehen. Um Switch wirklich nutzen zu können, besteht eine andere Alternative darin, eine benutzerdefinierte Aufzählung zu haben, die Ihren Typ in Ihren Klassen angibt.

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

Dies ist auch in BCL implementiert. Ein Beispiel ist MemberInfo.MemberTypes , ein anderes GetTypeCodefür primitive Typen wie:

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

Dies ist eine alternative Antwort, die Beiträge von JaredPar- und VirtLink-Antworten mit den folgenden Einschränkungen mischt:

  • Die Schalterkonstruktion verhält sich wie eine Funktion und empfängt Funktionen als Parameter für Fälle.
  • Stellt sicher, dass es ordnungsgemäß erstellt wurde und immer eine Standardfunktion vorhanden ist .
  • Es wird nach dem ersten Match zurückgegeben (wahr für JaredPar-Antwort, nicht wahr für VirtLink).

Verwendungszweck:

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

Code:

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
quelle
0

Ja - verwenden Sie einfach den etwas seltsam benannten "Pattern Matching" ab C # 7, um eine Übereinstimmung mit Klasse oder Struktur zu erzielen:

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

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

ich benutze

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

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

Sollte mit funktionieren

Fallart _:

mögen:

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
quelle
0

Wenn Sie die Klasse kennen, die Sie erwarten, aber noch kein Objekt haben, können Sie dies sogar tun:

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
quelle
0

Ab C # 8 können Sie den neuen Schalter noch präziser gestalten. Und mit der Discard-Option _ können Sie vermeiden, dass unnötige Variablen erstellt werden, wenn Sie diese nicht benötigen:

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

Rechnung und Versandliste sind Klassen und Dokument ist ein Objekt, das eine von beiden sein kann.

David
quelle