Existe uma alternativa melhor do que essa para 'ativar o tipo'?


331

Visto que o C # não pode switchem um tipo (que, segundo entendo, não foi adicionado como um caso especial, porque os isrelacionamentos significam que mais de uma distinta casepode ser aplicada), existe uma maneira melhor de simular a ativação de outro tipo que não seja esse?

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

18
Por curiosidade, por que você não usa polimorfismo?

18
@jeyoung aulas selado, e não vale a pena para situações ad-hoc
xyz



2
@ jeyoung: Uma situação típica em que o polimorfismo não pode ser usado é quando os tipos que estão sendo trocados não devem conhecer o código que contém a switchdeclaração. Um exemplo: o conjunto A contém um conjunto de objetos de dados (que não serão alterados, definidos em um documento de especificação ou algo semelhante). Os conjuntos B , C e D referem-se a A e fornecem uma conversão para os vários objetos de dados de A (por exemplo, uma serialização / desserialização para um formato específico). Você precisa espelhar toda a hierarquia de classes em B , C e D e usar fábricas, ou possui ...
OU Mapper

Respostas:


276

Definitivamente, a ativação de tipos está ausente no C # ( UPDATE: no C # 7 / VS 2017, a ativação de tipos é suportada - consulte a resposta de Zachary Yates abaixo ). Para fazer isso sem uma declaração if / else if / else grande, você precisará trabalhar com uma estrutura diferente. Eu escrevi um post de blog há algum tempo, detalhando como construir uma estrutura TypeSwitch.

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

Versão curta: TypeSwitch foi projetado para impedir a transmissão redundante e fornecer uma sintaxe semelhante a uma instrução switch / case normal. Por exemplo, aqui está TypeSwitch em ação em um evento de formulário padrão do 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"));

O código para TypeSwitch é realmente muito pequeno e pode ser facilmente inserido no seu projeto.

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

26
"type == entry.Target" também pode ser alterado para "entry.Target.IsAssignableFrom (type)" para levar em consideração tipos compatíveis (por exemplo, subclasses).
Mark Cidade

Alterou o código para usar "entry.Target.IsAssignableFrom (type)" para que as subclasses sejam suportadas.
Matt Howells

3
Uma coisa que talvez valha a pena notar é que (pelo que entendi) é necessário especificar a ação 'padrão' por último para garantir que todos os outros casos sejam verificados. Eu acredito que isso não é um requisito em um switch padrão - não que eu já tenha visto alguém tentar plantar um 'padrão' em qualquer outro lugar que não seja o fundo. Algumas opções à prova de falhas para isso podem ser: ordenar a matriz para garantir que o padrão seja o último (pouco desperdício) ou exibir o padrão em uma variável a ser processada após a foreach(que só aconteceria se uma correspondência não fosse encontrada)
musefan

E se o remetente for nulo? GetType irá lançar uma exceção
Jon

Duas sugestões: manipule a fonte nula chamando default ou lançando uma exceção e livre-se do booleano CaseInfoapenas checando o valor do tipo (se for nulo, é o padrão).
Felix K.

291

Com o C # 7 , fornecido com o Visual Studio 2017 (versão 15. *), você pode usar tipos em caseinstruções (correspondência de padrão):

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

Com o C # 6, você pode usar uma instrução switch com o operador nameof () (obrigado @Joey Adams):

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

Com o C # 5 e versões anteriores, você poderia usar uma instrução switch, mas precisará usar uma string mágica que contenha o nome do tipo ... o que não é particularmente favorável ao refator (obrigado @nukefusion)

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

1
isso funciona com maiúsculas e minúsculas typeof (string) .Name: ... ou deve ser com Valuetype?
21412 Tomer W W

3
Ofuscação pode quebrá-lo
Konrad Morawski

6
@nukefusion: Ou seja, a menos que você use o novo nameof()operador brilhante .
Joey Adams

21
Não gosto desta resposta porque nameof (NamespaceA.ClassC) == nameof (NamespaceB.ClassC) é verdadeiro.
ischas

7
(c # 7) você também pode usar sublinhado se não precisar acessar o objeto:case UnauthorizedException _:
Assaf S.

101

Uma opção é ter um dicionário de Typepara Action(ou algum outro delegado). Procure a ação com base no tipo e, em seguida, execute-a. Eu usei isso para fábricas antes de agora.


31
Nota menor: bom para correspondências 1: 1, mas pode ser um problema com herança e / ou interfaces - especialmente porque a ordem não é garantida para ser preservada com um dicionário. Mas ainda assim, é o jeito que eu faço em alguns lugares justos ;-p So +1
Marc Gravell

@ Marc: Como a herança ou as interfaces quebrariam nesse paradigma? Assumindo que a chave é um tipo, e a ação é um método, a herança ou as interfaces devem realmente forçar o Right Thing (TM), tanto quanto eu sei. Eu certamente entendo o problema com várias ações e falta de pedidos.
Harper Shelby

2
Eu usei essa técnica muito no passado, geralmente antes de mudar para um contêiner de IoC
Chris Canal

4
Essa técnica é dividida em herança e interfaces, porque você precisa de uma correspondência individual entre o objeto que está verificando e o delegado que está chamando. Qual das múltiplas interfaces de um objeto você deve tentar encontrar no dicionário?
Robert Rossney 18/11/08

5
Se você estiver criando um dicionário especificamente para esse fim, poderá sobrecarregar o indexador para retornar o valor do tipo de chave ou, se estiver faltando, sua superclasse, se estiver faltando, então essa superclasse, etc., até que não haja mais nada.
31511 Erik Forbes

49

Com a resposta de JaredPar na parte de trás da minha cabeça, escrevi uma variante de sua TypeSwitchclasse que usa inferência de tipo para obter uma sintaxe mais agradável:

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

Observe que a ordem dos Case()métodos é importante.


Obtenha o código completo e comentado da minha TypeSwitchturma . Esta é uma versão abreviada de trabalho:

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

Parece uma boa solução e queria ver o que mais você tinha a dizer sobre isso, mas o blog está morto.
Wes Grant

1
Droga, você está certo. Meu host está com alguns problemas desde uma hora. Eles estão trabalhando nisso. A postagem no meu blog é essencialmente a mesma que a resposta aqui, mas com um link para o código fonte completo.
Daniel AA Pelsmaeker

1
Adoro como isso reduz um monte de colchetes if a um simples interruptor "funcional". Bom trabalho!
James White

2
Você também pode adicionar um método de extensão para o caso inicial: public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSource. Isso permite que você digavalue.Case((C x) ...
Joey Adams

1
@ JoeyAdams: incorporei sua última sugestão, juntamente com algumas pequenas melhorias. No entanto, estou mantendo a sintaxe igual.
Daniel Daniel Pelsmaeker

14

Crie uma superclasse (S) e faça com que A e B a herdem. Em seguida, declare um método abstrato em S que todas as subclasses precisam implementar.

Fazendo isso, o método "foo" também pode alterar sua assinatura para Foo (S o), tornando-o seguro, e você não precisa lançar essa exceção feia.


Verdadeiro bruno, mas a pergunta não sugere isso. Você pode incluir isso em sua resposta, por Pablo.
Dana the Sane

A partir da pergunta, acho que A e B são genéricos o suficiente para serem A = String; B = Lista <int> por exemplo ...
bruno conde

13

Você pode usar a correspondência de padrões no C # 7 ou acima:

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

Obrigado por este! Também pode ser usado para detectar subclasses: if (this.TemplatedParent.GetType (). IsSubclassOf (typeof (RadGridView))) pode ser alterado para: switch (this.TemplatedParent.GetType ()) case var subRadGridView when subRadGridView.IsSubclassOf typeof (RadGridView)):
Flemming Bonde Kentved

Você está fazendo isso errado. Veja a resposta de Serge Intern e leia sobre o princípio de substituição de Liskov
0xF

8

Você realmente deveria estar sobrecarregando seu método, não tentando fazer a desambiguação por conta própria. A maioria das respostas até agora não leva em consideração as subclasses futuras, o que pode levar a problemas de manutenção realmente terríveis posteriormente.


3
A resolução de sobrecarga é determinada estaticamente para que simplesmente não funcione.
Neutrino

@ Neutrino: não há nada na pergunta que determine que o tipo não seja conhecido no momento da compilação. E, se for, uma sobrecarga faz muito mais sentido do que qualquer outra opção, dado o exemplo de código original do OP.
Peter Duniho

Eu acho que o fato de ele estar tentando usar uma declaração 'if' ou 'switch' para determinar o tipo é uma indicação bastante clara de que o tipo não é conhecido no momento da compilação.
Neutrino

@ Neutrino, lembro que, como apontou Sergey Berezovskiy, existe a palavra-chave dinâmica em C #, que representa um tipo que precisa ser resolvido dinamicamente (em tempo de execução, em vez de em tempo de compilação).
Davide Cannizzo 28/08/19

8

Se você estivesse usando o C # 4, poderia usar a nova funcionalidade dinâmica para obter uma alternativa interessante. Não estou dizendo que isso é melhor, na verdade parece muito provável que seria mais lento, mas tem certa elegância.

class Thing
{

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

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

}

E o uso:

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

A razão pela qual isso funciona é que uma chamada de método dinâmico C # 4 tem suas sobrecargas resolvidas em tempo de execução, em vez de tempo de compilação. Escrevi um pouco mais sobre essa idéia recentemente . Mais uma vez, gostaria de reiterar que isso provavelmente tem um desempenho pior do que todas as outras sugestões. Estou oferecendo isso simplesmente como curiosidade.


1
Eu tive a mesma ideia hoje. É aproximadamente três vezes mais lento que ligar o nome do tipo. É claro que o mais lento é relativo (para 60.000.000 de chamadas, apenas 4 segundos), e o código é muito mais legível que vale a pena.
Daryl


7

Para tipos internos, você pode usar a enumeração TypeCode. Observe que GetType () é meio lento, mas provavelmente não é relevante na maioria das situações.

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

Para tipos personalizados, você pode criar sua própria enumeração e uma interface ou uma classe base com propriedade ou método abstrato ...

Implementação de classe abstrata da propriedade

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

Implementação de classe abstrata do 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; }
}

Implementação da interface da propriedade

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

Implementação da interface do método

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

Um dos meus colegas de trabalho também me falou sobre isso: Isso tem a vantagem de você poder usá-lo para literalmente qualquer tipo de objeto, não apenas aqueles que você define. Tem a desvantagem de ser um pouco maior e mais lento.

Primeiro defina uma classe 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());
        }
    }
}

E então você pode usá-lo assim:

switch (TypeEnumerator.EnumerateType(someObject))
{
    case TypeEnumerator.TypeEnumeratorTypes._int:
        break;
    case TypeEnumerator.TypeEnumeratorTypes._string:
        break;
}

Obrigado por adicionar o TypeCode () - variante para tipos primitivos, porque mesmo o C # 7.0 - variante não funciona com aqueles (nem faz nameof () obviamente)
Ole Albers

6

Gostei do uso de digitação implícita do Virtlink para tornar a mudança muito mais legível, mas não gostei do fato de que o início não é possível e que estamos fazendo alocações. Vamos aumentar um pouco o desempenho.

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

Bem, isso faz meus dedos doerem. Vamos fazer isso no 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 um pouco o exemplo do 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());

Legível e rápido. Agora, como todo mundo continua apontando em suas respostas, e dada a natureza dessa pergunta, a ordem é importante na correspondência de tipos. Portanto:

  • Coloque os tipos de folhas primeiro, os tipos de base depois.
  • Para tipos de pares, coloque correspondências mais prováveis ​​primeiro para maximizar o desempenho.
  • Isso implica que não há necessidade de um caso padrão especial. Em vez disso, basta usar o tipo mais básico no lambda e colocá-lo por último.

5

Dada a herança facilita que um objeto seja reconhecido como mais de um tipo, acho que uma mudança pode levar a uma ambiguidade ruim. Por exemplo:

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 é uma string e um objeto. Eu acho que quando você escreve um, switch(foo)você espera que foo corresponda a um e apenas um doscase declarações. Com uma seleção de tipos, a ordem na qual você escreve suas instruções de caso pode alterar o resultado de toda a instrução de troca. Eu acho que isso estaria errado.

Você pode pensar em uma verificação do compilador nos tipos de uma instrução "typeswitch", verificando se os tipos enumerados não herdam uns dos outros. Isso ainda não existe.

foo is Tnão é o mesmo que foo.GetType() == typeof(T)!!



4

Outra maneira seria definir uma interface IThing e implementá-la nas duas classes, aqui está o trecho:

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

4

Conforme a especificação do C # 7.0, você pode declarar uma variável local com escopo definido em 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;
}

Essa é a melhor maneira de fazer uma coisa dessas, pois envolve apenas operações de conversão e empurre na pilha, que são as operações mais rápidas que um intérprete pode executar logo após operações e booleancondições bit a bit .

Comparando isso com a Dictionary<K, V>, aqui está muito menos uso de memória: manter um dicionário requer mais espaço na RAM e um pouco mais de computação pela CPU para criar duas matrizes (uma para chaves e outra para valores) e reunir códigos de hash para as chaves colocarem valores para suas respectivas chaves.

Portanto, até onde eu sei, não acredito que exista uma maneira mais rápida, a menos que você queira usar apenas um bloco if- then- elsecom o isoperador da seguinte maneira:

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.

3

Você pode criar métodos sobrecarregados:

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

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

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

E lance o argumento para dynamicdigitar, a fim de ignorar a verificação de tipo estático:

Foo((dynamic)something);

3

Os aprimoramentos em C # 8 da correspondência de padrões tornaram possível fazê-lo dessa maneira. Em alguns casos, ele faz o trabalho e é mais conciso.

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

2

Você está procurando Discriminated Unionsquais são os recursos de linguagem do F #, mas pode obter um efeito semelhante usando uma biblioteca que criei, chamada OneOf

https://github.com/mcintyre321/OneOf

A principal vantagem sobre switch(e ife exceptions as control flow) é que é seguro em tempo de compilação - não há manipulador padrão ou falha

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

Se você adicionar um terceiro item ao o, receberá um erro do compilador, pois precisará adicionar um manipulador Func dentro da chamada do switch.

Você também pode fazer um .Matchque retorne um valor, em vez de executar uma instrução:

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

2

Criar uma interface IFooable, em seguida, fazer seus Ae Bclasses para implementar um método comum, que por sua vez chama o método correspondente que você deseja:

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

Observe que é melhor usar asprimeiro a checagem ise depois a transmissão, pois assim você faz 2 lançamentos, por isso é mais caro.


2

Em tais casos, geralmente termino com uma lista de predicados e ações. Algo nesse 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);
    }
}

2

Depois de comparar as opções fornecidas por algumas respostas aqui aos recursos do F #, descobri que o F # tinha um suporte muito melhor para a comutação baseada em tipo (embora eu ainda esteja usando o C #).
Você pode querer ver aqui e aqui .


2
<insira o plugue para F # aqui>
Overlord Zurg

1

Eu criaria uma interface com qualquer nome e método que fizesse sentido para o seu switch, vamos chamá-los respectivamente: IDoableque diz para 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();
    }
}

e altere o método da seguinte maneira:

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

Pelo menos com isso, você está seguro no momento da compilação e suspeito que, em termos de desempenho, é melhor do que verificar o tipo em tempo de execução.


1

Com o C # 8 em diante, você pode torná-lo ainda mais conciso com o novo switch. E com o uso da opção de descarte _, você pode evitar a criação de variáveis ​​desnecessárias quando não precisar delas, assim:

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

Invoice e ShippingList são classes e document é um objeto que pode ser um deles.


0

Eu concordo com Jon sobre ter um hash de ações para o nome da classe. Se você mantiver seu padrão, considere usar a construção "as":

A a = o as A;
if (a != null) {
    a.Hop();
    return;
}
B b = o as B;
if (b != null) {
    b.Skip();
    return;
}
throw new ArgumentException("...");

A diferença é que, quando você usa o padrão, if (foo é Bar) {((Bar) foo) .Action (); } você está fazendo o tipo de conversão duas vezes. Agora, talvez o compilador otimize e faça isso funcionar apenas uma vez - mas eu não contaria com isso.


1
Eu realmente não gosto de vários pontos de saída (retornos), mas se você quiser continuar com isso, adicione "if (o == null) throw" no início, pois mais tarde você não saberá se o elenco não foi bem-sucedido ou o o objeto era nulo.
Sunny Milenov 18/11/2008

0

Como Pablo sugere, a abordagem da interface é quase sempre a coisa certa a ser feita para lidar com isso. Para realmente utilizar a opção, outra alternativa é ter uma enumeração personalizada indicando seu tipo em suas 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:
        //......
    }
}

Isso também é implementado no BCL. Um exemplo é MemberInfo.MemberTypes , outro é 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 ......
    }
}

0

Esta é uma resposta alternativa que combina contribuições das respostas JaredPar e VirtLink, com as seguintes restrições:

  • A construção do switch se comporta como uma função e recebe funções como parâmetros para os casos.
  • Garante que ele seja construído corretamente e sempre exista uma função padrão .
  • Ele retorna após a primeira correspondência (verdadeiro para a resposta JaredPar, não verdadeiro para o VirtLink 1).

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

0

Sim - basta usar a "correspondência de padrões" ligeiramente estranhamente denominada do C # 7 para cima para corresponder à classe ou estrutura:

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

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

0

eu uso

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

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

0

Deve trabalhar com

caso-tipo _:

gostar:

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

0

Se você conhece a classe que está esperando, mas ainda não possui um objeto, pode fazer isso:

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";
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.