Чи є краща альтернатива, ніж ця, для "переключення на тип"?


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 (наприклад, серіалізація / десеріалізація до певного формату). Вам або доведеться відобразити всю ієрархію класів у 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 призначений для запобігання надлишкової трансляції та надання синтаксису, подібного до звичайного оператора перемикача / випадку. Наприклад, ось 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
        };
    }
}

26
"type == entry.Target" також можна змінити на "entry.Target.IsAssignableFrom (type)", щоб врахувати сумісні типи (наприклад, підкласи).
Марк Сідаде

Змінено код на використання "entry.Target.IsAssignableFrom (type)" ", щоб підкласи підтримувалися.
Метт Хоуеллс

3
Можливо, варто відзначити, що (наскільки я розумію) потрібно вказати дію "за замовчуванням" останньою, щоб забезпечити перевірку всіх інших випадків. Я вважаю, що це не є вимогою до стандартного перемикача - не те, що я коли-небудь бачив, щоб хто-небудь намагався посадити "за замовчуванням" в будь-якому місці, крім дна в будь-якому випадку. Кілька безпечних варіантів для цього може бути: замовити масив, щоб переконатися, що типовий параметр є останнім (трохи марнотратним), або вивести за замовчуванням змінну, яка обробляється після foreach(що коли-небудь трапиться, якщо збігу не знайдено)
musefan

Що робити, якщо відправник недійсний? GetType згенерує виняток
Jon

Дві пропозиції: Обробіть нульове джерело, зателефонувавши за замовчуванням або викинувши виняток, і позбудьтесь булевого рівня в CaseInfoпросто, перевіривши відповідність типу типу (якщо його нуль - це за замовчуванням).
Фелікс К.

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 ви можете використовувати оператор перемикання з оператором nameof () (спасибі @Joey Adams):

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

З C # 5 і новішими версіями ви можете використовувати оператор перемикання, але вам доведеться використовувати магічну рядок, що містить ім'я типу ... що не особливо зручно для рефактора (спасибі @nukefusion)

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

1
це працює з типом case (string). Ім'я: ... або це має бути з Valuetype?
Томер Ш

3
Обмеження може зламати це
Конрад Моравський

6
@nukefusion: Тобто, якщо ви не використовуєте новий блискучий nameof()оператор .
Joey Adams

21
Мені не подобається ця відповідь, тому що nameof (NamespaceA.ClassC) == nameof (NamespaceB.ClassC) є істинним.
ischas

7
(c # 7) ви також можете підкреслити, якщо вам не потрібен доступ до об'єкта:case UnauthorizedException _:
Assaf S.

101

Одним з варіантів є створення словника з Typeдо Action(або який - або інший делегат). Знайдіть дію залежно від типу та виконайте її. Я раніше це використовував на фабриках.


31
Незначна примітка: добре для поєдинків 1: 1, але це може бути болем із спадщиною та / або інтерфейсами - тим більше, що порядок не гарантується збереженням словника. Але все-таки, це так, як я це роблю в небагатьох місцях ;-p Так +1
Марк Гравелл

@Marc: Як у цій парадигмі порушиться спадкування або інтерфейси? Якщо припустити, що ключ - це тип, а дія - це метод, то успадкування або інтерфейси насправді повинні змусити правильну річ (TM), наскільки я можу сказати. Я, безумовно, розумію проблему з кількома діями та відсутністю замовлень.
Харпер Шелбі

2
Я використовував цю техніку багато раніше, як правило, перед тим, як перейти до контейнера IoC
Кріс Канал

4
Ця методика розбивається на спадкування та інтерфейси, оскільки вам потрібна відповідність один на один між об'єктом, який ви перевіряєте, та делегатом, якого ви телефонуєте. Який із декількох інтерфейсів об’єкта слід спробувати знайти у словнику?
Роберт Россні

5
Якщо ви будуєте словник спеціально для цієї мети, ви можете перевантажити індексатор, щоб повернути значення типу ключа, або якщо він відсутній, то його суперклас, якщо цього не вистачає, то цей суперклас тощо, поки нічого не залишиться.
Ерік Форбс

49

З відповіддю ДжаредПара в потилиці, я написав варіант його 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
Дарн, ти маєш рацію. У мого веб-господаря виникають деякі проблеми вже з години. Вони над цим працюють. Публікація в моєму щоденнику фактично така ж, як відповідь тут, але з посиланням на повний вихідний код.
Daniel AA Pelsmaeker

1
Любіть, як це зменшує купу, якщо дужки, до простого "функціонального" перемикача. Хороша робота!
Джеймс Уайт

2
Крім того, можна додати метод розширення для вихідного випадку: public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSource. Це дозволяє вам сказатиvalue.Case((C x) ...
Joey Adams

1
@JoeyAdams: Я включив вашу останню пропозицію, а також деякі невеликі вдосконалення. Однак синтаксис я зберігаю однаковий.
Даніель А.А. Пелсмейкер

14

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

Зробивши це, метод "foo" також може змінити свій підпис на Foo (S o), зробивши його безпечним, і вам не потрібно кидати це потворне виключення.


Справжня бруно, але питання цього не підказує. Ви можете включити це у свою відповідь, хоча Пабло.
Dana Sane

З питання я думаю, що 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;
}

Дякую за це! Можна також використовувати для виявлення підкласів: якщо (this.TemplatedParent.GetType (). IsSubclassOf (typeof (RadGridView))) може бути змінено на: switch (this.TemplatedParent.GetType ()) case var subRadGridView коли subRadGridView.IsSubSISSubSISSubSisSubSub typeof (RadGridView)):
Flemming Bonde Kentved

Ви робите це неправильно. Дивіться відповідь Сергія Інтерна та читайте про принцип заміни
Ліскова

8

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


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


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 - варіант не працює з тими (явно не має іменіof ())
Оле Альберс

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

Зрозумілий та швидкий. Тепер, як усі продовжують вказувати у своїх відповідях та враховуючи характер цього питання, порядок є важливим у відповідності типу. Тому:

  • Покладіть спочатку типи листя, пізніше базові.
  • Для рівних типів спочатку додайте більше ймовірних відповідностей, щоб отримати максимальну ефективність.
  • Це означає, що немає необхідності у спеціальному випадку за замовчуванням. Натомість просто використовуйте базовий тип у лямбда та поставте його останньою.

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 - це рядок і об'єкт. Я думаю, коли ви пишете a, switch(foo)ви очікуєте, що foo збігається з одним і єдиним із нихcase тверджень. За допомогою перемикача на типи порядок, коли ви пишете виписки вашої справи, можливо, може змінити результат усієї заяви перемикача. Я думаю, що це було б неправильно.

Можна подумати про компілятор-перевірку типів оператора "typewitch", перевіривши, чи перелічені типи не успадковують один одного. Однак цього не існує.

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

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

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 всередині виклику комутатора.

Ви також можете зробити a, .Matchякий повертає значення, а не виконувати операцію:

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

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

2

Після порівняння варіантів декількох відповідей, наданих функціям F #, я виявив, що F # має кращу підтримку типового перемикання (хоча я все ще дотримуюся C #).
Ви можете побачити тут і тут .


2
<вставити штекер для F # тут>
Overlord Zurg

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 ви можете зробити його ще більш коротким за допомогою нового перемикача. І з використанням опції відкидання _ ви можете уникнути створення непотрібних змінних, коли вони вам не потрібні, як це:

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

Рахунок-фактура та ShippingList - це класи, а документ є об'єктом, який може бути будь-яким з них.


0

Я погоджуюся з Джоном про те, щоб зробити хеш дій щодо імені класу. Якщо ви зберігаєте свою схему, ви можете замість цього використати конструкцію "як":

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 є Bar) {((Bar) foo) .Action (); } Ви робите кастинг типів двічі. Тепер, можливо, компілятор оптимізує і виконує цю роботу лише один раз, але я б на це не розраховував.


1
Мені дійсно не подобається кілька точок виходу (повернення), але якщо ви хочете дотримуватися цього, додайте на початку "if (o == null)", оскільки пізніше ви не дізнаєтесь, чи не вдався виступ, чи об’єкт був недійсним.
Сонячний Міленов

0

Як пропонує Пабло, інтерфейсний підхід майже завжди є правильним для вирішення цього питання. Щоб реально використовувати комутатор, іншою альтернативою є встановлення спеціального перерахунку, що позначає ваш тип у ваших класах.

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

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

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";
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.