Есть ли лучшая альтернатива, чем эта, чтобы «включить тип»?

331

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

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
Из любопытства, почему вы просто не используете полиморфизм?
18
@jeyoung запечатал классы, и это не стоит для специальных ситуаций
xyz
2
@jeyoung: типичная ситуация, когда полиморфизм не может быть использован, - это когда переключаемые типы не должны знать код, содержащий switchоператор. Один пример: сборка A содержит набор объектов данных (которые не будут изменены, определенные в документе спецификации или тому подобное). Сборки B , C и D каждая ссылаются на A и обеспечивают преобразование для различных объектов данных из A (например, сериализация / десериализация в некоторый конкретный формат). Вы либо должны отразить всю иерархию классов в B , C и D и использовать фабрики, либо у вас есть ...
ИЛИ Mapper

Ответы:

276

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

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

Короткая версия: 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
        };
    }
}
JaredPar
источник
26
«type == entry.Target» также можно изменить на «entry.Target.IsAssignableFrom (type)», чтобы учитывать совместимые типы (например, подклассы).
Марк Сидаде
Изменен код для использования «entry.Target.IsAssignableFrom (type)», чтобы подклассы поддерживали.
Мэтт Хауэллс
3
Стоит отметить, что (из того, что я понимаю) необходимо указать последнее действие «по умолчанию», чтобы обеспечить проверку всех остальных случаев. Я полагаю, что это не является обязательным требованием для стандартного коммутатора - не то, чтобы я когда-либо видел, чтобы кто-нибудь пытался установить «дефолт» где-либо, кроме дна. Пару отказоустойчивых опций для этого можно было бы упорядочить массив, чтобы гарантировать, что значение по умолчанию является последним (немного расточительным), или добавить значение по умолчанию в переменную, которая будет обработана после foreach(что будет происходить, только если совпадение не найдено)
Musefan
Что если отправитель нулевой? GetType сгенерирует исключение
Jon
Два предложения: Обрабатывать нулевой источник, вызывая default или выбрасывая исключение, и избавляться от логического значения в CaseInfo, просто проверяя значение типа (если оно равно null, это значение по умолчанию).
Феликс К.
291

В 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;
}
Захари Йейтс
источник
1
это работает с case typeof (string) .Name: ... или это должно быть с Valuetype?
Томер W
3
Запутывание может сломать это
Конрад Моравский
6
@nukefusion: То есть, если вы не используете блестящий новый nameof()оператор .
Джои Адамс,
21
Мне не нравится этот ответ, потому что nameof (NamespaceA.ClassC) == nameof (NamespaceB.ClassC) имеет значение true.
Ищ
7
(c # 7) вы также можете использовать подчеркивание, если вам не нужен доступ к объекту:case UnauthorizedException _:
Assaf S.
101

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

Джон Скит
источник
31
Незначительное замечание: хорошо для соответствий 1: 1, но может быть проблемой с наследованием и / или интерфейсами - тем более что порядок не гарантируется для словаря. Но, тем не менее, я так делаю в нескольких местах ;-p1 +1
Марк Гравелл
@Marc: Как наследование или интерфейсы сломаются в этой парадигме? Если предположить, что ключ является типом, а действие - методом, то, насколько я могу судить, наследование или интерфейсы должны на самом деле форсировать правильную вещь (TM). Я, конечно, понимаю проблему с несколькими действиями и отсутствием порядка.
Харпер Шелби
2
Я использовал эту технику много раз в прошлом, обычно до перехода на контейнер IoC
Крис Канал
4
Этот метод не подходит для наследования и интерфейсов, потому что вам нужно взаимно-однозначное соответствие между проверяемым объектом и вызываемым делегатом. Какой из многочисленных интерфейсов объекта вы должны попытаться найти в словаре?
Роберт Россни
5
Если вы создаете словарь специально для этой цели, вы можете перегрузить индексатор, чтобы он возвращал значение типа ключа, или, если он отсутствует, то его суперкласс, если он отсутствует, то этот суперкласс и т. Д., Пока не останется ничего.
Эрик Форбс
49

С ответом JaredPar в затылке я написал вариант его 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);
        }
    }
}
Даниэль А.А. Пельсмакер
источник
Выглядит как хорошее решение и хотел посмотреть, что еще ты хотел сказать по этому поводу, но блог мертв.
Уэс Грант
1
Черт, ты прав. Мой веб-хостинг имеет некоторые проблемы с часа. Они работают над этим. Пост в моем блоге по сути такой же, как ответ здесь, но со ссылкой на полный исходный код.
Даниэль А.А. Пельсмакер
1
Мне нравится, как это сводит кучу скобок к простому «функциональному» переключателю. Хорошая работа!
Джеймс Уайт
2
Кроме того, можно добавить метод расширения для исходного случая: public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSource. Это позволяет вам сказатьvalue.Case((C x) ...
Джои Адамс
1
@JoeyAdams: я включил ваше последнее предложение вместе с некоторыми небольшими улучшениями. Тем не менее, я придерживаюсь синтаксиса.
Даниэль А.А. Пельсмакер
14

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

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

Пабло Фернандес
источник
Правда бруно, но вопрос не говорит об этом. Вы можете включить это в свой ответ, хотя Пабло.
Дана Саня
Из вопроса я думаю, что A и B достаточно универсальны, чтобы они могли быть A = String; B = перечислите <int>, например ...
bruno conde
13

Вы можете использовать сопоставление с образцом в C # 7 или выше:

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
источник
Спасибо за это! Может также использоваться для обнаружения подклассов: if (this.TemplatedParent.GetType (). IsSubclassOf (typeof (RadGridView))) можно изменить на: switch (this.TemplatedParent.GetType ()) case var subRadGridView при subRadGridView.IsSubclassOf ( typeof (RadGridView)):
Флемминг Бонд Кентвед
Ты делаешь это неправильно. Посмотрите ответ Сергея Интернна и прочитайте о принципе замены Лискова
0xF
8

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

sep332
источник
3
Разрешение перегрузки определяется статически, так что просто не будет работать вообще.
Нейтрино
@Neutrino: в этом вопросе нет ничего, что указывало бы, что тип не известен во время компиляции. И если это так, перегрузка имеет куда больше смысла, чем любая другая опция, учитывая оригинальный пример кода OP.
Питер Дунихо
Я думаю, что тот факт, что он пытается использовать оператор «if» или «switch» для определения типа, является довольно четким признаком того, что тип не известен во время компиляции.
Нейтрино
@Neutrino, я помню, что, как отметил Сергей Березовский, в C # есть динамическое ключевое слово, представляющее тип, который должен решаться динамически (во время выполнения, а не во время компиляции).
Давиде
8

Если бы вы использовали 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 перегрузки разрешаются во время выполнения, а не во время компиляции. Я написал немного больше об этой идее совсем недавно . Опять же, я просто хотел бы повторить, что это, вероятно, работает хуже, чем все другие предложения, я предлагаю это просто из любопытства.

Пол Батум
источник
1
У меня была та же идея сегодня. Это примерно в 3 раза медленнее, чем переключение имени типа. Конечно, медленнее относительный (для 60 000 000 звонков, всего 4 секунды), и код гораздо более читабелен, это того стоит.
Дэрил
8

Да, благодаря C # 7 этого можно достичь. Вот как это делается (с использованием шаблона выражения ):

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());
}
Серж Интерн
источник
1
+1, см. Документацию по функциям C # 7
Флориан Кох
7

Для встроенных типов вы можете использовать перечисление 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;
}
Эдвард Нед Харви
источник
Спасибо за добавление TypeCode () - варианта для примитивных типов, потому что даже вариант C # 7.0 - не работает с ними (и не делает nameof (), очевидно)
Ole Albers
6

Мне понравилось использование 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());

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

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

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

Случай 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)!!

Эврен Кузукуоглу
источник
4

Другой способ - определить интерфейс IThing, а затем реализовать его в обоих классах. Вот фрагмент кода:

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
источник
4

Согласно спецификации C # 7.0, вы можете объявить локальную переменную в caseобласти 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;
}

Это лучший способ сделать это, потому что он включает в себя только операции приведения и операции над стеком, которые являются самыми быстрыми операциями, которые интерпретатор может выполнить сразу после побитовых операций и booleanусловий.

Сравнивая это с a Dictionary<K, V>, мы получаем гораздо меньшее использование памяти: для хранения словаря требуется больше места в оперативной памяти, а процессор требует больше вычислений для создания двух массивов (один для ключей и другой для значений) и сбора хэш-кодов для ключей, которые нужно поместить значения их соответствующих ключей.

Итак, насколько я знаю, я не верю, что более быстрый способ мог бы существовать, если вы не хотите использовать только if- then- elseблок с isоператором следующим образом:

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

Вы можете создавать перегруженные методы:

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

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

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

И приведите аргумент к dynamicтипу, чтобы обойти статическую проверку типов:

Foo((dynamic)something);
Сергей Березовский
источник
3

C # 8 улучшений сопоставления с образцом позволили сделать это следующим образом. В некоторых случаях это делает работу и более кратким.

        public Animal Animal { get; set; }
        ...
        var animalName = Animal switch
        {
            Cat cat => "Tom",
            Mouse mouse => "Jerry",
            _ => "unknown"
        };
PilgrimViis
источник
2

Вы ищете Discriminated Unionsкакую-либо языковую особенность F #, но вы можете добиться аналогичного эффекта, используя созданную мной библиотеку под названием OneOf

https://github.com/mcintyre321/OneOf

Основное преимущество перед switchifи exceptions as control flow) заключается в том, что он безопасен во время компиляции - здесь нет обработчика по умолчанию или сбой

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

Если вы добавите третий элемент в o, вы получите ошибку компилятора, так как вам нужно добавить обработчик Func внутри вызова switch.

Вы также можете сделать, .Matchкоторый возвращает значение, а не выполняет инструкцию:

double Area(OneOf<Square, Circle> o)
{
    return o.Match(
        square => square.Length * square.Length,
        circle => Math.PI * circle.Radius * circle.Radius
    );
}
mcintyre321
источник
2

Создайте интерфейс IFooable, затем создайте свой класс Aи Bкласс для реализации общего метода, который, в свою очередь, вызывает соответствующий метод, который вы хотите:

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

Обратите внимание, что лучше использовать asвместо этого сначала проверку с помощью isприведения, а затем приведение, так как вы делаете 2 приведения, так что это дороже.

Солнечный Миленов
источник
2

В таких случаях я обычно получаю список предикатов и действий. Что-то в этом роде:

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
источник
2

После сравнения вариантов нескольких ответов, представленных здесь, с возможностями F #, я обнаружил, что в F # улучшена поддержка переключения на основе типов (хотя я все еще придерживаюсь C #).
Возможно, вы захотите увидеть здесь и здесь .

Марк Гравелл
источник
2
<вставить штекер для F # здесь>
Повелитель Цург
1

Я хотел бы создать интерфейс с любым именем и именем метода, которое имело бы смысл для вашего коммутатора, давайте назовем их соответственно: IDoableэто говорит о необходимости реализации 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();
    }
}

и измените метод следующим образом:

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

По крайней мере, с этим вы в безопасности во время компиляции, и я подозреваю, что с точки зрения производительности это лучше, чем проверка типа во время выполнения.

Керри Перре
источник
1

С C # 8 вы можете сделать его еще более лаконичным с новым коммутатором. А с помощью опции discard _ вы можете избежать создания бесполезных переменных, когда они вам не нужны, например:

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

Invoice и ShippingList являются классами, а document является объектом, который может быть любым из них.

Дэвид
источник
0

Я согласен с Джоном по поводу наличия хеша действий для имени класса. Если вы сохраняете свой шаблон, вы можете использовать вместо этого конструкцию «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("...");

Разница в том, что когда вы используете шаблон, if (foo is Bar) {((Bar) foo) .Action (); } вы делаете приведение типов дважды. Теперь, может быть, компилятор оптимизирует и выполнит эту работу только один раз - но я бы на это не рассчитывал.

плинтус
источник
1
Мне действительно не нравятся множественные точки выхода (возвраты), но если вы хотите придерживаться этого, добавьте «if (o == null) throw» в начале, так как позже вы не будете знать, если приведение не было успешным, или объект был нулевым.
Солнечный Миленов
0

Как полагает Пабло, интерфейсный подход - почти всегда правильная вещь, чтобы справиться с этим. Чтобы по-настоящему использовать switch, другой альтернативой является создание собственного перечисления, обозначающего ваш тип в ваших классах.

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

Это также реализовано в BCL. Один пример - MemberInfo.MemberTypes , другой - GetTypeCodeдля примитивных типов, например:

void Foo(object o)
{
    switch (Type.GetTypeCode(o.GetType())) // for IConvertible, just o.GetTypeCode()
    {
        case TypeCode.Int16:
        case TypeCode.Int32:
        //etc ......
    }
}
Навфал
источник
0

Это альтернативный ответ, который сочетает вклады ответов JaredPar и VirtLink со следующими ограничениями:

  • Конструкция переключателя ведет себя как функция и получает функции в качестве параметров для случаев.
  • Гарантирует, что он правильно построен, и всегда существует функция по умолчанию .
  • Он возвращается после первого матча (верно для JaredPar ответа, не верно для VirtLink одного).

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

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

Код:

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
источник
0

Да - просто используйте слегка странное название «сопоставление с образцом» из C # 7 и выше, чтобы сопоставить класс или структуру:

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

switch (concrete1)
{
    case ObjectImplementation1 c1: return "type 1";         
    case ObjectImplementation2 c2: return "type 2";         
}
Джеймс Харкорт
источник
0

я использую

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

        if (t == typeof(CategoryDataStore))
            return (T)DependencyService.Get<IDataStore<ItemCategory>>();
        else
            return default(T);
    }
mdimai666
источник
0

Должен работать с

тип дела _:

лайк:

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

Если вы знаете ожидаемый класс, но у вас все еще нет объекта, вы даже можете сделать это:

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
источник