Есть ли лучшая альтернатива, чем эта, чтобы «включить тип»? - PullRequest
299 голосов
/ 18 ноября 2008

Видя, как C # не может переключить на тип (который, как я понимаю, не был добавлен в качестве особого случая, потому что отношения - это означает, что более чем один отдельный случай может применить), есть ли лучший способ для имитации переключения типа, чем этот?

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

Ответы [ 28 ]

268 голосов
/ 18 ноября 2008

Включение типов определенно отсутствует в C # ( ОБНОВЛЕНИЕ: в C # 7 / VS 2017 поддерживается переключение типов - см. Ответ Захария Йейтса ниже ). Чтобы сделать это без большого оператора if / else if / else, вам нужно будет работать с другой структурой. Некоторое время назад я написал сообщение в блоге, в котором подробно рассказывалось, как построить структуру TypeSwitch.

http://blogs.msdn.com/jaredpar/archive/2008/05/16/switching-on-types.aspx

Короткая версия: TypeSwitch разработан для предотвращения избыточного приведения и дает синтаксис, похожий на обычный оператор switch / case. Например, вот TypeSwitch в действии для стандартного события формы 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"));

Код для TypeSwitch на самом деле довольно маленький и его легко можно вставить в ваш проект.

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
        };
    }
}
233 голосов
/ 18 ноября 2008

С C # 7 , который поставляется с Visual Studio 2017 (выпуск 15. *), вы можете использовать типы в операторах case (сопоставление с образцом):

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

В C # 6 вы можете использовать оператор switch с оператором nameof () (спасибо @Joey Adams):

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

В C # 5 и более ранних версиях вы можете использовать оператор switch, но вам придется использовать волшебную строку, содержащую имя типа ... которая не особенно удобна для рефакторинга (спасибо @nukefusion)

switch(o.GetType().Name) {
  case "AType":
    break;
}
100 голосов
/ 18 ноября 2008

Один из вариантов - иметь словарь от Type до Action (или какой-либо другой делегат). Найдите действие, основанное на типе, и затем выполните его. Я использовал это для заводов до сих пор.

49 голосов
/ 05 апреля 2012

С ответом ДжаредПара в затылке Я написал вариант его TypeSwitch класса, который использует вывод типов для более приятного синтаксиса:

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

Обратите внимание, что порядок Case() методов важен.


Получите полный и закомментированный код для моего TypeSwitch класса . Это рабочая сокращенная версия:

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);
        }
    }
}
14 голосов
/ 18 ноября 2008

Создайте суперкласс (S) и сделайте так, чтобы A и B наследовали его. Затем объявите абстрактный метод на S, который должен реализовать каждый подкласс.

Делая это, метод "foo" также может изменить свою подпись на Foo (S o), что делает его безопасным для типов, и вам не нужно бросать это безобразное исключение.

8 голосов
/ 19 ноября 2008

Если бы вы использовали C # 4, вы могли бы использовать новую динамическую функциональность, чтобы создать интересную альтернативу. Я не говорю, что это лучше, на самом деле кажется, что это будет медленнее, но в нем есть определенная элегантность.

class Thing
{

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

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

}

И использование:

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

Причина, по которой это работает, заключается в том, что при вызове динамического метода C # 4 перегрузки разрешаются во время выполнения, а не во время компиляции. Я написал немного больше об этой идее совсем недавно . Опять же, я просто хотел бы повторить, что это, вероятно, работает хуже, чем все другие предложения, я предлагаю это просто из любопытства.

7 голосов
/ 18 ноября 2008

Вы действительно должны перегружать свой метод, а не пытаться выполнять устранение неоднозначности самостоятельно. Большинство ответов до сих пор не учитывают будущие подклассы, что в дальнейшем может привести к действительно ужасным проблемам обслуживания.

7 голосов
/ 15 ноября 2013

Для встроенных типов вы можете использовать перечисление TypeCode. Обратите внимание, что GetType () довольно медленный, но, вероятно, не подходит в большинстве ситуаций.

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

Для пользовательских типов вы можете создать собственное перечисление, а также интерфейс или базовый класс с абстрактным свойством или методом ...

Реализация класса абстрактного свойства

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

Реализация класса абстрактного метода

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

Интерфейс реализации свойства

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

Интерфейсная реализация метода

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

Один из моих коллег только что рассказал мне об этом: у него есть то преимущество, что вы можете использовать его буквально для любого типа объекта, а не только для того, который вы определяете. Недостатком является то, что он немного больше и медленнее.

Сначала определите статический класс следующим образом:

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

И тогда вы можете использовать его так:

switch (TypeEnumerator.EnumerateType(someObject))
{
    case TypeEnumerator.TypeEnumeratorTypes._int:
        break;
    case TypeEnumerator.TypeEnumeratorTypes._string:
        break;
}
6 голосов
/ 18 июня 2013

Мне понравилось использование Virtlink неявной типизации , чтобы сделать переключение намного более читабельным, но мне не понравилось, что ранний выход невозможен, и что мы делаем распределение. Давайте немного прибавим.

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

Ну, это заставляет мои пальцы болеть. Давайте сделаем это в 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#>
}

Немного подправив пример 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());

Читается и быстро. Теперь, когда все продолжают указывать в своих ответах и ​​учитывая природу этого вопроса, порядок соответствия важен при сопоставлении типов. Поэтому:

  • Сначала ставьте типы листьев, потом базовые.
  • Для пировых типов сначала ставьте более вероятные совпадения, чтобы максимизировать производительность.
  • Это означает, что нет необходимости в специальном случае по умолчанию. Вместо этого просто используйте самый базовый тип в лямбде и поместите его последним.
5 голосов
/ 06 мая 2011

Учитывая, что наследование облегчает распознавание объекта как более одного типа, я думаю, что переключение может привести к плохой неопределенности. Например:

Дело 1

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

Дело 2

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

Потому что s - это строка , а - объект. Я думаю, что когда вы пишете switch(foo), вы ожидаете, что foo будет соответствовать одному и только одному из операторов case. С переключением типов порядок, в котором вы пишете свои операторы case, может изменить результат всего оператора switch. Я думаю, что это было бы неправильно.

Можно подумать о проверке компилятором типов оператора «typeswitch», проверяющего, что перечисляемые типы не наследуются друг от друга. Этого не существует, хотя.

foo is T - это не то же самое, что foo.GetType() == typeof(T) !!

...