Existe-t-il une meilleure alternative que celle-ci pour «activer le type»?

331

Étant donné que C # ne peut pas switchsur un type (qui, je suppose, n'a pas été ajouté en tant que cas spécial car les isrelations signifient que plusieurs distinctes casepeuvent s'appliquer), existe-t-il une meilleure façon de simuler la commutation sur un type autre que celui-ci?

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
la source
18
Par curiosité, pourquoi n'utilisez-vous pas simplement le polymorphisme?
18
@jeyoung a scellé les cours, et ça ne vaut pas le coup pour les situations ponctuelles
xyz
2
@jeyoung: Une situation typique où le polymorphisme ne peut pas être utilisé est lorsque les types qui sont commutés ne doivent pas connaître le code qui contient l' switchinstruction. Un exemple: l'assemblage A contient un ensemble d'objets de données (qui ne va pas changer, défini dans un document de spécification ou autre). Les assemblages B , C et D font chacun référence à A et fournissent une conversion pour les divers objets de données de A (par exemple, une sérialisation / désérialisation vers un format particulier). Vous devez soit refléter la hiérarchie de classe entière en B , C et D , et utiliser des usines, soit vous avez ...
OR Mapper

Réponses:

276

L'activation des types fait définitivement défaut en C # ( MISE À JOUR: dans C # 7 / VS 2017, l'activation des types est prise en charge - voir la réponse de Zachary Yates ci-dessous ). Pour ce faire sans une grande instruction if / else if / else, vous devrez travailler avec une structure différente. J'ai écrit un article de blog il y a un certain temps, détaillant comment construire une structure TypeSwitch.

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

Version courte: TypeSwitch est conçu pour empêcher la conversion redondante et donner une syntaxe similaire à une instruction switch / case normale. Par exemple, voici TypeSwitch en action sur un événement de formulaire Windows standard

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

Le code de TypeSwitch est en fait assez petit et peut facilement être mis dans votre projet.

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
la source
26
"type == entry.Target" peut également être remplacé par "entry.Target.IsAssignableFrom (type)" pour prendre en compte les types compatibles (par exemple, les sous-classes).
Mark Cidade
Modification du code pour utiliser "entry.Target.IsAssignableFrom (type)" afin que les sous-classes soient prises en charge.
Matt Howells
3
Une chose à noter est que (d'après ce que je comprends), il est nécessaire de spécifier l'action «par défaut» en dernier pour garantir que tous les autres cas sont vérifiés. Je crois que ce n'est pas une exigence dans un commutateur standard - pas que j'ai jamais vu quelqu'un essayer de planter un "défaut" ailleurs que dans le bas de toute façon. Un couple d'options de sécurité pour cela pourrait être d'ordonner le tableau pour s'assurer que la valeur par défaut est la dernière (peu de gaspillage) ou d'insérer la valeur par défaut dans une variable à traiter après le foreach(ce qui ne se produirait que si aucune correspondance n'était trouvée)
musefan
Et si l'expéditeur est nul? GetType lèvera une exception
Jon
Deux suggestions: gérer la source null en appelant default ou en lançant une exception et se débarrasser du booléen dans CaseInfoen vérifiant simplement la valeur de type (si sa valeur null est la valeur par défaut).
Felix K.
291

Avec C # 7 , fourni avec Visual Studio 2017 (version 15. *), vous pouvez utiliser les types dans les caseinstructions (correspondance de modèle):

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

Avec C # 6, vous pouvez utiliser une instruction switch avec l' opérateur nameof () (merci @Joey Adams):

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

Avec C # 5 et versions antérieures, vous pouvez utiliser une instruction switch, mais vous devrez utiliser une chaîne magique contenant le nom du type ... qui n'est pas particulièrement adaptée aux refactoriseurs (merci @nukefusion)

switch(o.GetType().Name) {
  case "AType":
    break;
}
Zachary Yates
la source
1
cela fonctionne-t-il avec caseof type (chaîne). Nom: ... ou doit-il être avec Valuetype?
Tomer W
3
L'obscurcissement peut le briser
Konrad Morawski
6
@nukefusion: Autrement dit, sauf si vous utilisez le nouvel nameof()opérateur brillant .
Joey Adams
21
Je n'aime pas cette réponse car nameof (NamespaceA.ClassC) == nameof (NamespaceB.ClassC) est vrai.
ischas
7
(c # 7) vous pouvez également utiliser le trait de soulignement si vous n'avez pas besoin d'accéder à l'objet:case UnauthorizedException _:
Assaf S.
101

Une option consiste à avoir un dictionnaire de Typeà Action(ou un autre délégué). Recherchez l'action en fonction du type, puis exécutez-la. Je l'ai déjà utilisé pour les usines.

Jon Skeet
la source
31
Note mineure: bon pour les correspondances 1: 1, mais peut être un problème d'héritage et / ou d'interfaces - d'autant plus que l'ordre n'est pas garanti d'être préservé avec un dictionnaire. Mais quand même, c'est comme ça que je le fais dans pas mal d'endroits ;-p Donc +1
Marc Gravell
@Marc: Comment l'héritage ou les interfaces se briseraient-ils dans ce paradigme? En supposant que la clé est un type et que l'action est une méthode, l'héritage ou les interfaces devraient en fait forcer la bonne chose (TM), pour autant que je sache. Je comprends certainement le problème des actions multiples et du manque de commande.
Harper Shelby
2
J'ai beaucoup utilisé cette technique dans le passé, généralement avant de passer à un conteneur IoC
Chris Canal
4
Cette technique se décompose pour l'héritage et les interfaces, car vous avez besoin d'une correspondance biunivoque entre l'objet que vous vérifiez et le délégué que vous appelez. Laquelle des multiples interfaces d'un objet devriez-vous essayer de trouver dans le dictionnaire?
Robert Rossney
5
Si vous créez un dictionnaire spécifiquement à cet effet, vous pouvez surcharger l'indexeur pour renvoyer la valeur du type de clé, ou s'il manque, alors sa superclasse, si elle manque, alors cette superclasse, etc., jusqu'à ce qu'il ne reste plus rien.
Erik Forbes
49

Avec la réponse de JaredPar au fond de ma tête, j'ai écrit une variante de sa TypeSwitchclasse qui utilise l'inférence de type pour une syntaxe plus agréable:

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

Notez que l'ordre des Case()méthodes est important.


Obtenez le code complet et commenté pour ma TypeSwitchclasse . Ceci est une version abrégée de travail:

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
la source
On dirait une bonne solution et je voulais voir ce que vous aviez d'autre à dire à ce sujet mais le blog est mort.
Wes Grant
1
Bon sang, tu as raison. Mon hébergeur rencontre des problèmes depuis une heure. Ils y travaillent. Le post sur mon blog est essentiellement le même que la réponse ici, mais avec un lien vers le code source complet.
Daniel AA Pelsmaeker
1
J'adore la façon dont cela réduit un tas de crochets if à un simple commutateur "fonctionnel". Bon travail!
James White
2
Vous pouvez également ajouter une méthode d'extension pour le cas initial: public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSource. Cela vous permet de direvalue.Case((C x) ...
Joey Adams
1
@JoeyAdams: J'ai incorporé votre dernière suggestion, ainsi que quelques petites améliorations. Cependant, je garde la même syntaxe.
Daniel AA Pelsmaeker
14

Créez une superclasse (S) et faites-en hériter A et B. Déclarez ensuite une méthode abstraite sur S que chaque sous-classe doit implémenter.

En faisant cela, la méthode "foo" peut également changer sa signature en Foo (S o), ce qui rend le type sûr, et vous n'avez pas besoin de lever cette horrible exception.

Pablo Fernandez
la source
Vrai bruno, mais la question ne le suggère pas. Vous pourriez inclure cela dans votre réponse par Pablo.
Dana the Sane
D'après la question, je pense que A et B sont suffisamment génériques pour pouvoir être A = String; B = Liste <int> par exemple ...
bruno conde
13

Vous pouvez utiliser la correspondance de motifs en C # 7 ou supérieur:

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
la source
Merci pour celui-ci! Peut également être utilisé pour détecter des sous-classes: if (this.TemplatedParent.GetType (). IsSubclassOf (typeof (RadGridView))) peut être changé en: switch (this.TemplatedParent.GetType ()) case var subRadGridView when subRadGridView.IsSubclassOf ( typeof (RadGridView)):
Flemming Bonde Kentved
Tu le fais incorectement. Voir la réponse de Serge Intern et lire sur le principe de substitution de Liskov
0xF
8

Vous devriez vraiment surcharger votre méthode, ne pas essayer de faire vous-même la désambiguïsation. Jusqu'à présent, la plupart des réponses ne prennent pas en compte les futures sous-classes, ce qui peut entraîner des problèmes de maintenance très graves ultérieurement.

sep332
la source
3
La résolution de surcharge est déterminée statiquement afin que cela ne fonctionne pas du tout.
Neutrino
@Neutrino: il n'y a rien dans la question qui dicte que le type n'est pas connu au moment de la compilation. Et si c'est le cas, une surcharge est beaucoup plus logique que toute autre option, étant donné l'exemple de code original de l'OP.
Peter Duniho
Je pense que le fait qu'il essaie d'utiliser une instruction «if» ou «switch» pour déterminer le type est une indication assez claire que le type n'est pas connu au moment de la compilation.
Neutrino
@Neutrino, je me souviens de vous que, comme l'a souligné Sergey Berezovskiy, il y a le mot-clé dynamique en C #, qui représente un type qui doit être résolu dynamiquement (au moment de l'exécution, plutôt qu'au moment de la compilation).
Davide Cannizzo
8

Si vous utilisiez C # 4, vous pourriez utiliser la nouvelle fonctionnalité dynamique pour obtenir une alternative intéressante. Je ne dis pas que c'est mieux, en fait, il semble très probable que ce serait plus lent, mais cela a une certaine élégance.

class Thing
{

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

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

}

Et l'utilisation:

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

La raison pour laquelle cela fonctionne est qu'un appel de méthode dynamique C # 4 a ses surcharges résolues au moment de l'exécution plutôt qu'au moment de la compilation. J'ai écrit un peu plus sur cette idée tout récemment . Encore une fois, je voudrais simplement répéter que cela fonctionne probablement moins bien que toutes les autres suggestions, je le propose simplement par curiosité.

Paul Batum
la source
1
J'ai eu la même idée aujourd'hui. C'est environ 3 fois plus lent que d'activer le nom du type. Bien sûr, le ralentissement est relatif (pour 60 000 000 d'appels, seulement 4 secondes), et le code est tellement plus lisible qu'il en vaut la peine.
Daryl
8

Oui, grâce au C # 7 qui peut être atteint. Voici comment cela se fait (en utilisant un modèle d'expression ):

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
la source
1
+1, voir la documentation des fonctionnalités C # 7
Florian Koch
7

Pour les types intégrés, vous pouvez utiliser l'énumération TypeCode. Veuillez noter que GetType () est un peu lent, mais probablement pas pertinent dans la plupart des situations.

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

Pour les types personnalisés, vous pouvez créer votre propre énumération et une interface ou une classe de base avec une propriété ou une méthode abstraite ...

Implémentation de classe abstraite de propriété

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

Implémentation de classe abstraite de méthode

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

Implémentation de l'interface de la propriété

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

Implémentation de l'interface de la méthode

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

Un de mes collègues vient de m'en parler aussi: cela a l'avantage que vous pouvez l'utiliser pour n'importe quel type d'objet, pas seulement ceux que vous définissez. Il a l'inconvénient d'être un peu plus gros et plus lent.

Définissez d'abord une classe statique comme celle-ci:

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

Et puis vous pouvez l'utiliser comme ceci:

switch (TypeEnumerator.EnumerateType(someObject))
{
    case TypeEnumerator.TypeEnumeratorTypes._int:
        break;
    case TypeEnumerator.TypeEnumeratorTypes._string:
        break;
}
Edward Ned Harvey
la source
Merci d'avoir ajouté la variante TypeCode () pour les types primitifs, car même la variante C # 7.0 - ne fonctionne pas avec ceux-ci (ni nameof () évidemment)
Ole Albers
6

J'ai aimé l' utilisation par Virtlink du typage implicite pour rendre le commutateur beaucoup plus lisible, mais je n'aimais pas qu'une sortie anticipée ne soit pas possible, et que nous fassions des allocations. Tournons un peu 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.
}

Eh bien, ça me fait mal aux doigts. Faisons-le dans 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#>
}

Ajuster un peu l'exemple de 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());

Lisible et rapide. Maintenant, comme tout le monde le souligne dans ses réponses, et compte tenu de la nature de cette question, l'ordre est important dans la correspondance des types. Par conséquent:

  • Mettez les types de feuilles en premier, les types de base plus tard.
  • Pour les types de pairs, placez d'abord les correspondances les plus probables pour maximiser les performances.
  • Cela implique qu'il n'y a pas besoin d'un cas par défaut spécial. À la place, utilisez simplement le type le plus bas dans le lambda et mettez-le en dernier.
scobi
la source
5

Étant donné que l'héritage facilite la reconnaissance d'un objet comme plusieurs types, je pense qu'un commutateur pourrait conduire à une mauvaise ambiguïté. Par exemple:

Cas 1

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

Cas 2

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

Parce que s est une chaîne et un objet. Je pense que lorsque vous écrivez un, switch(foo)vous vous attendez à ce que foo corresponde à une et une seule des casedéclarations. Avec un commutateur sur les types, l'ordre dans lequel vous écrivez vos instructions case peut éventuellement changer le résultat de l'instruction switch entière. Je pense que ce serait faux.

Vous pourriez penser à une vérification du compilateur sur les types d'une instruction "typeswitch", vérifiant que les types énumérés n'héritent pas les uns des autres. Cela n'existe cependant pas.

foo is Tn'est pas le même que foo.GetType() == typeof(T)!!

Evren Kuzucuoglu
la source
4

Je le ferais

  • utiliser la surcharge de méthode (tout comme x0n ), ou
  • utiliser des sous-classes (tout comme Pablo ), ou
  • appliquer le modèle de visiteur .
Jonas Kongslund
la source
4

Une autre façon serait de définir une interface IThing puis de l'implémenter dans les deux classes, voici le 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
la source
4

Selon la spécification C # 7.0, vous pouvez déclarer une variable locale de portée dans a casede a 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;
}

C'est la meilleure façon de faire une telle chose car elle implique simplement des opérations de transtypage et de push-on-the-stack, qui sont les opérations les plus rapides qu'un interprète peut exécuter juste après des opérations et des booleanconditions au niveau du bit .

En comparant cela à un Dictionary<K, V>, voici beaucoup moins d'utilisation de la mémoire: la tenue d'un dictionnaire nécessite plus d'espace dans la RAM et un peu plus de calcul par le CPU pour créer deux tableaux (un pour les clés et l'autre pour les valeurs) et rassembler les codes de hachage pour les clés à mettre valeurs à leurs clés respectives.

Donc, pour autant que je sache, je ne pense pas qu'un moyen plus rapide puisse exister à moins que vous ne vouliez utiliser qu'un bloc if- then- elseavec l' isopérateur comme suit:

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
la source
3

Vous pouvez créer des méthodes surchargées:

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

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

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

Et transformez l'argument en dynamictype afin de contourner la vérification de type statique:

Foo((dynamic)something);
Sergey Berezovskiy
la source
3

Les améliorations C # 8 de la correspondance de motifs ont permis de le faire comme ceci. Dans certains cas, il fait le travail et est plus concis.

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

Vous recherchez les Discriminated Unionsfonctionnalités linguistiques de F #, mais vous pouvez obtenir un effet similaire en utilisant une bibliothèque que j'ai créée, appelée OneOf

https://github.com/mcintyre321/OneOf

L'avantage majeur par rapport à switch(et ifet exceptions as control flow) est qu'il est sûr au moment de la compilation - il n'y a pas de gestionnaire par défaut ou de chute

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

Si vous ajoutez un troisième élément à o, vous obtiendrez une erreur de compilation car vous devez ajouter un gestionnaire Func dans l'appel de commutateur.

Vous pouvez également faire un .Matchqui renvoie une valeur, plutôt que d'exécuter une instruction:

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

Créez une interface IFooable, puis créez vos classes Aet Bpour implémenter une méthode commune, qui à son tour appelle la méthode correspondante que vous souhaitez:

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

Notez qu'il vaut mieux utiliser à la asplace d'abord vérifier ispuis lancer, car de cette façon vous faites 2 lancers, donc c'est plus cher.

Sunny Milenov
la source
2

Dans de tels cas, je me retrouve généralement avec une liste de prédicats et d'actions. Quelque chose dans ce sens:

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
la source
2

Après avoir comparé les options quelques réponses fournies ici aux fonctionnalités F #, j'ai découvert que F # avait un meilleur support pour la commutation basée sur le type (bien que je m'en tienne toujours à C #).
Vous voudrez peut-être voir ici et ici .

Marc Gravell
la source
2
<insérer la fiche pour F # ici>
Overlord Zurg
1

Je créerais une interface avec n'importe quel nom et nom de méthode qui aurait du sens pour votre commutateur, appelons-les respectivement: IDoablecela indique de mettre en œuvre 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();
    }
}

et changez la méthode comme suit:

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

Au moins avec cela, vous êtes en sécurité au moment de la compilation et je soupçonne que sur le plan des performances, c'est mieux que de vérifier le type au moment de l'exécution.

Kerry Perret
la source
1

Avec C # 8, vous pouvez le rendre encore plus concis avec le nouveau commutateur. Et avec l'utilisation de l'option de suppression _, vous pouvez éviter de créer des variables inutiles lorsque vous n'en avez pas besoin, comme ceci:

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

Invoice et ShippingList sont des classes et le document est un objet qui peut être l'un ou l'autre.

David
la source
0

Je suis d'accord avec Jon pour avoir un hachage d'actions au nom de la classe. Si vous conservez votre modèle, vous voudrez peut-être envisager d'utiliser la construction "as" à la place:

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 différence est que lorsque vous utilisez le motif si (foo est Bar) {((Bar) foo) .Action (); } vous effectuez deux fois le casting de caractères. Maintenant, peut-être que le compilateur optimisera et ne fera ce travail qu'une seule fois - mais je ne compterais pas là-dessus.

socle
la source
1
Je n'aime vraiment pas les points de sortie multiples (retours), mais si vous voulez vous en tenir à cela, ajoutez "if (o == null) throw" au début, car plus tard, vous ne saurez pas si la distribution a échoué, ou l'objet était nul.
Sunny Milenov
0

Comme le suggère Pablo, l'approche par interface est presque toujours la bonne chose à faire pour gérer cela. Pour vraiment utiliser switch, une autre alternative est d'avoir une énumération personnalisée indiquant votre type dans vos classes.

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

Ceci est également implémenté dans BCL. Un exemple est MemberInfo.MemberTypes , un autre est GetTypeCodepour les types primitifs, comme:

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

Il s'agit d'une réponse alternative qui mélange les contributions des réponses JaredPar et VirtLink, avec les contraintes suivantes:

  • La construction du commutateur se comporte comme une fonction et reçoit des fonctions comme paramètres des cas.
  • S'assure qu'il est correctement construit et qu'il existe toujours une fonction par défaut .
  • Il revient après la première correspondance (vrai pour la réponse JaredPar, pas vrai pour VirtLink one).

Usage:

 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
la source
0

Oui - utilisez simplement la "correspondance de motifs" légèrement bizarre à partir de C # 7 pour faire correspondre la classe ou la structure:

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

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

j'utilise

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

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

Devrait fonctionner avec

type de boîtier _:

comme:

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
la source
0

Si vous connaissez la classe que vous attendez mais que vous n'avez toujours pas d'objet, vous pouvez même le faire:

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
la source