¿Existe una alternativa mejor que esta para 'cambiar el tipo'?

331

Al ver que C # no puede switchen un Tipo (que, según creo, no se agregó como un caso especial porque las isrelaciones significan que casepodría aplicarse más de un distintivo ), ¿hay una mejor manera de simular el cambio de tipo que no sea este?

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
fuente
18
Por curiosidad, ¿por qué no usas el polimorfismo?
18
@jeyoung clases selladas, y no vale la pena para situaciones ad-hoc
xyz
2
@jeyoung: Una situación típica en la que no se puede usar el polimorfismo es cuando los tipos que se cambian no deben conocer el código que contiene la switchdeclaración. Un ejemplo: el conjunto A contiene un conjunto de objetos de datos (que no va a cambiar, definidos en un documento de especificación o similar). Los ensamblajes B , C y D hacen referencia a A y proporcionan una conversión para los diversos objetos de datos de A (por ejemplo, una serialización / deserialización a algún formato particular). Tienes que reflejar toda la jerarquía de clases en B , C y D , y usar fábricas, o tienes ...
O Mapper

Respuestas:

276

La activación de tipos definitivamente falta en C # ( ACTUALIZACIÓN: en C # 7 / VS 2017 se admite la activación de tipos; consulte la respuesta de Zachary Yates a continuación ). Para hacer esto sin una gran declaración if / else if / else, deberá trabajar con una estructura diferente. Escribí una publicación de blog hace un tiempo detallando cómo construir una estructura TypeSwitch.

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

Versión corta: TypeSwitch está diseñado para evitar la conversión redundante y proporcionar una sintaxis que es similar a una declaración de cambio / caso normal. Por ejemplo, aquí está TypeSwitch en acción en un evento de formulario estándar de 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"));

El código para TypeSwitch es bastante pequeño y se puede incluir fácilmente en su proyecto.

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
fuente
26
"type == entry.Target" también se puede cambiar a "entry.Target.IsAssignableFrom (type)" para tener en cuenta los tipos compatibles (por ejemplo, subclases).
Mark Cidade
Se modificó el código para usar "entry.Target.IsAssignableFrom (type)" para que las subclases sean compatibles.
Matt Howells
3
Una cosa que tal vez valga la pena señalar es que (por lo que entiendo) es necesario especificar la acción 'predeterminada' en último lugar para garantizar que se verifiquen todos los demás casos. Creo que esto no es un requisito en un interruptor estándar, no es que haya visto a alguien intentar plantar un 'defecto' en cualquier lugar que no sea la parte inferior de todos modos. Un par de opciones a prueba de fallas para esto podría ser ordenar la matriz para garantizar que el valor predeterminado sea el último (poco inútil) o mostrar el valor predeterminado en una variable que se procesará después de foreach(lo que solo sucedería si no se encuentra una coincidencia)
musefan
¿Qué pasa si el remitente es nulo? GetType lanzará una excepción
Jon
Dos sugerencias: maneje la fuente nula llamando a default o lanzando una excepción y elimine el valor booleano CaseInfosimplemente verificando el valor de tipo (si es nulo, es el valor predeterminado).
Felix K.
291

Con C # 7 , que se envió con Visual Studio 2017 (Versión 15. *), puede usar Tipos en casedeclaraciones (coincidencia de patrones):

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

Con C # 6, puede usar una instrucción switch con el operador nameof () (gracias @Joey Adams):

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

Con C # 5 y versiones anteriores, podría usar una instrucción de cambio, pero tendrá que usar una cadena mágica que contenga el nombre del tipo ... que no es particularmente fácil de refactorizar (gracias @nukefusion)

switch(o.GetType().Name) {
  case "AType":
    break;
}
Zachary Yates
fuente
1
¿funciona esto con case typeof (string) .Name: ... o tiene que ser con Valuetype?
Tomer W
3
La ofuscación puede romperlo
Konrad Morawski
66
@nukefusion: Es decir, a menos que use el nuevo nameof()operador brillante .
Joey Adams
21
No me gusta esta respuesta porque nameof (NamespaceA.ClassC) == nameof (NamespaceB.ClassC) es verdadero.
Ischas
77
(c # 7) también puede usar el guión bajo si no necesita acceso al objeto:case UnauthorizedException _:
Assaf S.
101

Una opción es tener un diccionario desde Typehasta Action(o algún otro delegado). Busque la acción según el tipo y luego ejecútela. He usado esto para fábricas antes ahora.

Jon Skeet
fuente
31
Nota menor: buena para coincidencias 1: 1, pero podría ser un problema con la herencia y / o las interfaces, especialmente porque no se garantiza que el orden se conserve con un diccionario. Pero aún así, es la forma en que lo hago en unos pocos lugares ;-p Entonces +1
Marc Gravell
@Marc: ¿Cómo se romperían la herencia o las interfaces en este paradigma? Suponiendo que la clave es un tipo, y la acción es un método, entonces la herencia o las interfaces deberían forzar a Right Thing (TM), por lo que puedo decir. Ciertamente entiendo el problema con múltiples acciones y falta de orden.
Harper Shelby
2
He usado mucho esta técnica en el pasado, generalmente antes de mudarme a un contenedor de IoC
Chris Canal
44
Esta técnica se desglosa por herencia e interfaces porque necesita una correspondencia uno a uno entre el objeto que está verificando y el delegado al que está llamando. ¿Cuál de las múltiples interfaces de un objeto debería intentar encontrar en el diccionario?
Robert Rossney
55
Si está creando un diccionario específicamente para este propósito, podría sobrecargar el indexador para devolver el valor del tipo de clave, o si falta, entonces su superclase, si falta, esa superclase, etc., hasta que no quede nada.
Erik Forbes
49

Con la respuesta de JaredPar en mi cabeza, escribí una variante de su TypeSwitchclase que usa inferencia de tipos para una sintaxis más agradable:

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

Tenga en cuenta que el orden de los Case()métodos es importante.


Obtenga el código completo y comentado para mi TypeSwitchclase . Esta es una versión abreviada que funciona:

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
fuente
Parece una buena solución y quería ver qué más tenía que decir al respecto, pero el blog está muerto.
Wes Grant
1
Maldición, tienes razón. Mi webhost tiene algunos problemas desde hace una hora. Están trabajando en eso. La publicación en mi blog es esencialmente la misma que la respuesta aquí, pero con un enlace al código fuente completo.
Daniel AA Pelsmaeker
1
Me encanta cómo esto reduce un montón de paréntesis si a un simple interruptor "funcional". ¡Buen trabajo!
James White
2
También puede agregar un método de extensión para el caso inicial: public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSource. Esto te permite decirvalue.Case((C x) ...
Joey Adams
1
@JoeyAdams: incorporé su última sugerencia, junto con algunas pequeñas mejoras. Sin embargo, mantengo la sintaxis igual.
Daniel AA Pelsmaeker
14

Crea una superclase (S) y haz que A y B hereden de ella. Luego declare un método abstracto en S que cada subclase necesita implementar.

Al hacer esto, el método "foo" también puede cambiar su firma a Foo (S o), lo que lo hace seguro, y no necesita lanzar esa fea excepción.

Pablo Fernández
fuente
Verdadero Bruno, pero la pregunta no sugiere eso. Podrías incluir eso en tu respuesta, aunque Pablo.
Dana the Sane
De la pregunta, creo que A y B son lo suficientemente genéricos como para que puedan ser A = String; B = Lista <int> por ejemplo ...
bruno conde
13

Puede usar la coincidencia de patrones en C # 7 o superior:

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
fuente
¡Gracias por esto! También se puede usar para detectar subclases: if (this.TemplatedParent.GetType (). IsSubclassOf (typeof (RadGridView))) se puede cambiar a: switch (this.TemplatedParent.GetType ()) caso var subRadGridView cuando subRadGridView.IsSubclassOf ( typeof (RadGridView)):
Flemming Bonde Kentved
Lo estas haciendo mal. Vea la respuesta de Serge Intern y lea sobre el principio de sustitución de Liskov
0xF
8

Realmente debería estar sobrecargando su método, no tratando de hacer la desambiguación usted mismo. La mayoría de las respuestas hasta ahora no tienen en cuenta las subclases futuras, lo que puede conducir a problemas de mantenimiento realmente terribles más adelante.

sep332
fuente
3
La resolución de sobrecarga se determina estáticamente para que simplemente no funcione en absoluto.
Neutrino
@Neutrino: no hay nada en la pregunta que indique que el tipo no se conoce en el momento de la compilación. Y si es así, una sobrecarga tiene mucho más sentido que cualquier otra opción, dado el ejemplo de código original del OP.
Peter Duniho
Creo que el hecho de que esté tratando de usar una declaración 'if' o 'switch' para determinar el tipo es una indicación bastante clara de que el tipo no se conoce en el momento de la compilación.
Neutrino
@Neutrino, recuerdo que, como señaló Sergey Berezovskiy, existe la palabra clave dinámica en C #, que representa un tipo que debe resolverse dinámicamente (en tiempo de ejecución, en lugar de tiempo de compilación).
Davide Cannizzo
8

Si estaba utilizando C # 4, podría hacer uso de la nueva funcionalidad dinámica para lograr una alternativa interesante. No digo que esto sea mejor, de hecho, parece muy probable que sea más lento, pero tiene cierta elegancia.

class Thing
{

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

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

}

Y el uso:

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

La razón por la que esto funciona es que una invocación de método dinámico C # 4 tiene sus sobrecargas resueltas en tiempo de ejecución en lugar de tiempo de compilación. Recientemente escribí un poco más sobre esta idea . Una vez más, me gustaría reiterar que esto probablemente funciona peor que todas las otras sugerencias, lo estoy ofreciendo simplemente como curiosidad.

Paul Batum
fuente
1
Tuve la misma idea hoy. Es aproximadamente 3 veces más lento que activar el nombre del tipo. Por supuesto, más lento es relativo (para 60,000,000 de llamadas, solo 4 segundos), y el código es mucho más legible que vale la pena.
Daryl
8

Sí, gracias a C # 7 que se puede lograr. Así es como se hace (usando el patrón de expresión ):

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
fuente
1
+1, consulte la documentación para las características de C # 7
Florian Koch
7

Para los tipos integrados, puede usar la enumeración TypeCode. Tenga en cuenta que GetType () es un poco lento, pero probablemente no es relevante en la mayoría de las situaciones.

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

Para los tipos personalizados, puede crear su propia enumeración, y una interfaz o una clase base con propiedad o método abstracto ...

Implementación de clase abstracta de propiedad

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

Implementación de clase abstracta del método

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

Implementación de interfaz de propiedad

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

Implementación de interfaz del método

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

Uno de mis compañeros de trabajo también me contó sobre esto: tiene la ventaja de que puede usarlo literalmente para cualquier tipo de objeto, no solo para los que defina. Tiene la desventaja de ser un poco más grande y lento.

Primero defina una clase estática como esta:

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

Y luego puedes usarlo así:

switch (TypeEnumerator.EnumerateType(someObject))
{
    case TypeEnumerator.TypeEnumeratorTypes._int:
        break;
    case TypeEnumerator.TypeEnumeratorTypes._string:
        break;
}
Edward Ned Harvey
fuente
Gracias por agregar la variante TypeCode () para los tipos primitivos, porque incluso la variante C # 7.0 no funciona con esos (tampoco lo hace nameof () obviamente)
Ole Albers
6

Me gustó el uso de la escritura implícita de Virtlink para hacer que el cambio sea mucho más legible, pero no me gustó que no sea posible una salida anticipada, y que estamos haciendo asignaciones. Subamos un poco el rendimiento.

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

Bueno, eso me duele los dedos. Hagámoslo en 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#>
}

Ajustando un poco el ejemplo 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());

Legible y rápido. Ahora, como todo el mundo sigue señalando en sus respuestas, y dada la naturaleza de esta pregunta, el orden es importante en la coincidencia de tipos. Por lo tanto:

  • Ponga los tipos de hojas primero, los tipos de base después.
  • Para los tipos de pares, coloque primero las coincidencias más probables para maximizar el rendimiento.
  • Esto implica que no hay necesidad de un caso especial por defecto. En su lugar, simplemente use el tipo más básico en la lambda y póngalo al final.
scobi
fuente
5

Dado que la herencia facilita que un objeto sea reconocido como más de un tipo, creo que un cambio podría conducir a una mala ambigüedad. Por ejemplo:

Caso 1

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

Caso 2

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

Porque s es una cadena y un objeto. Creo que cuando escribes un switch(foo), esperas que coincida con una y solo una de las casedeclaraciones. Con un cambio de tipos, el orden en el que escribe sus declaraciones de caso podría cambiar el resultado de toda la declaración de cambio. Creo que eso estaría mal.

Podría pensar en una verificación del compilador sobre los tipos de una declaración de "cambio de tipos", verificando que los tipos enumerados no se hereden entre sí. Sin embargo, eso no existe.

foo is Tno es lo mismo que foo.GetType() == typeof(T)!!

Evren Kuzucuoglu
fuente
4

Otra forma sería definir una interfaz IThing y luego implementarla en ambas clases, aquí está el fragmento:

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
fuente
4

Según la especificación de C # 7.0, puede declarar una variable local con un alcance 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;
}

Esta es la mejor manera de hacer tal cosa, ya que involucra solo operaciones de conversión y push-on-the-stack, que son las operaciones más rápidas que un intérprete puede ejecutar justo después de las operaciones y booleancondiciones bit a bit .

Comparando esto con un Dictionary<K, V>, aquí hay mucho menos uso de memoria: mantener un diccionario requiere más espacio en la RAM y algunos cálculos más por parte de la CPU para crear dos matrices (una para las claves y la otra para los valores) y recopilar códigos hash para que las claves pongan valores a sus respectivas claves.

Por lo tanto, para la medida de lo sé, no creo que una manera más rápida podría existir a menos que desee utilizar sólo una if- then- elsebloque con el isoperador de la siguiente manera:

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

Puedes crear métodos sobrecargados:

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

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

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

Y envíe el argumento a dynamicescribir para evitar la comprobación de tipo estático:

Foo((dynamic)something);
Sergey Berezovskiy
fuente
3

Las mejoras de C # 8 en la coincidencia de patrones permitieron hacerlo así. En algunos casos hace el trabajo y es más conciso.

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

Estás buscando Discriminated Unionscuáles son las características del lenguaje de F #, pero puedes lograr un efecto similar usando una biblioteca que hice, llamada OneOf

https://github.com/mcintyre321/OneOf

La principal ventaja sobre switch(y if, y exceptions as control flow) es que se trata de tiempo de compilación seguro - no hay ningún controlador predeterminado o caer a través

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

Si agrega un tercer elemento a o, obtendrá un error de compilación ya que debe agregar un controlador Func dentro de la llamada del conmutador.

También puede hacer un .Matchque devuelve un valor, en lugar de ejecutar una declaración:

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

Cree una interfaz IFooable, luego haga que su Ay Bclases implementen un método común, que a su vez llama al método correspondiente que desea:

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

Tenga en cuenta que es mejor usar en aslugar de verificar primero isy luego lanzar, ya que de esa manera hace 2 lanzamientos, por lo que es más costoso.

Sunny Milenov
fuente
2

En tales casos, generalmente termino con una lista de predicados y acciones. Algo en este sentido:

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

Después de haber comparado las opciones que algunas de las respuestas aquí proporcionadas a las características de F #, descubrí que F # tiene un mejor soporte para la conmutación basada en tipos (aunque todavía me quedo con C #).
Es posible que desee ver aquí y aquí .

Marc Gravell
fuente
2
<inserte el enchufe para F # aquí>
Overlord Zurg
1

Crearía una interfaz con cualquier nombre y nombre de método que tuviera sentido para su conmutador, llamémoslos respectivamente: IDoableeso indica implementar 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();
    }
}

y cambie el método de la siguiente manera:

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

Al menos con eso, está a salvo en el momento de la compilación y sospecho que en cuanto al rendimiento es mejor que verificar el tipo en tiempo de ejecución.

Kerry Perret
fuente
1

Con C # 8 en adelante, puede hacerlo aún más conciso con el nuevo interruptor. Y con el uso de la opción de descarte _ puede evitar crear variables innecesarias cuando no las necesite, de esta manera:

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

Factura y ShippingList son clases y el documento es un objeto que puede ser cualquiera de ellos.

David
fuente
0

Estoy de acuerdo con Jon sobre tener un hash de acciones para el nombre de la clase. Si mantiene su patrón, es posible que desee considerar el uso de la construcción "como" en su lugar:

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 diferencia es que cuando usa el patrón if (foo es Bar) {((Bar) foo) .Action (); } estás haciendo el tipo de conversión dos veces. Ahora tal vez el compilador se optimizará y solo hará ese trabajo una vez, pero no contaría con eso.

pedestal
fuente
1
Realmente no me gustan los puntos de salida múltiples (retornos), pero si desea seguir con esto, agregue "if (o == null) throw" al principio, ya que más adelante no sabrá si el lanzamiento no tuvo éxito o si El objeto era nulo.
Sunny Milenov
0

Como Pablo sugiere, el enfoque de interfaz es casi siempre lo correcto para manejar esto. Para utilizar realmente el interruptor, otra alternativa es tener una enumeración personalizada que indique su tipo en sus clases.

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

Esto también se implementa en BCL. Un ejemplo es MemberInfo.MemberTypes , otro es GetTypeCodepara tipos primitivos, como:

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

Esta es una respuesta alternativa que combina las contribuciones de las respuestas de JaredPar y VirtLink, con las siguientes restricciones:

  • La construcción del interruptor se comporta como una función y recibe funciones como parámetros para casos.
  • Asegura que se construye correctamente y que siempre existe una función predeterminada .
  • Se vuelve tras el primer partido (cierto para JaredPar respuesta, no es cierto para VirtLink uno).

Uso:

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

Código:

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

Sí, solo use la "coincidencia de patrones" un poco extrañamente nombrada de C # 7 hacia arriba para que coincida con la clase o estructura:

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

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

yo suelo

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

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

Debería trabajar con

tipo de caso _:

me gusta:

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

Si conoce la clase que espera pero aún no tiene un objeto, incluso puede hacer esto:

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
fuente