Есть ли лучшая альтернатива этому, чтобы "включить тип"?
Видя, что С# не может 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());
}
}
Ответы
Ответ 1
Переключение типов определенно отсутствует в С# (ОБНОВЛЕНИЕ: в С# 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
};
}
}
Ответ 2
В С# 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));
}
В С# 6 вы можете использовать оператор switch с оператором nameof() (спасибо @Joey Adams):
switch(o.GetType().Name) {
case nameof(AType):
break;
case nameof(BType):
break;
}
В С# 5 и более ранних версиях вы можете использовать оператор switch, но вам придется использовать магическую строку, содержащую имя типа... которая не особенно удобна для рефакторинга (спасибо @nukefusion)
switch(o.GetType().Name) {
case "AType":
break;
}
Ответ 3
Один из вариантов - иметь словарь от Type
до Action
(или другого делегата). Посмотрите действие, основанное на типе, и затем выполните его. Раньше я использовал это для заводов.
Ответ 4
С ответом 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);
}
}
}
Ответ 5
Создайте суперкласс (S) и сделайте A и B наследованием от него. Затем объявите абстрактный метод на S, который должен реализовать каждый подкласс.
Выполнение этого метода "foo" также может изменить свою подпись на Foo (S o), сделав его безопасным, и вам не нужно бросать это уродливое исключение.
Ответ 6
Если вы использовали С# 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);
Причина этого в том, что вызов динамического метода С# 4 имеет свои перегрузки, разрешенные во время выполнения, а не во время компиляции. Я написал немного больше об этой идее совсем недавно. Опять же, я хотел бы повторить, что это, вероятно, хуже, чем все другие предложения, я предлагаю это просто как любопытство.
Ответ 7
Вы действительно должны перегружать свой метод, не пытаясь сделать самосознание самостоятельно. Большинство ответов до сих пор не учитывают будущие подклассы, что может привести к серьезным проблемам обслуживания в дальнейшем.
Ответ 8
Для встроенных типов вы можете использовать перечисление 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;
}
Ответ 9
Мне понравился 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());
Читаемость и быстрота. Теперь, когда все продолжают указывать в своих ответах и учитывая характер этого вопроса, порядок важен при сопоставлении типов. Поэтому:
- Сначала введите типы листов, базовые типы позже.
- Для типов сверстников, скорее всего, сначала совпадения, чтобы максимизировать perf.
- Это означает, что нет необходимости в специальном случае по умолчанию. Вместо этого просто используйте самый базовый тип в лямбда и поместите его последним.
Ответ 10
Данное наследование облегчает распознавание объекта более чем одним типом, я думаю, что переключатель может привести к плохой двусмысленности. Например:
Случай 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
. При использовании типов переключателей порядок, в котором вы пишете ваши заявления о случаях, может изменить результат всего оператора switch. Я думаю, что это было бы неправильно.
Вы можете подумать о компиляторе-проверке типов оператора "typewitch", проверяя, что перечисленные типы не наследуются друг от друга. Однако этого не существует.
foo is T
не совпадает с foo.GetType() == typeof(T)
!!
Ответ 11
Да, благодаря С# 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());
}
Ответ 12
Вы можете использовать сопоставление с образцом в С# 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;
}
Ответ 13
Я бы либо
- используйте метод перегрузки (как x0n) или
- используйте подклассы (например, Pablo) или
- примените шаблон .
Ответ 14
Другим способом было бы определить интерфейс 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();
}
}
Ответ 15
Вы можете создавать перегруженные методы:
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);
Ответ 16
В таких случаях я обычно получаю список предикатов и действий. Что-то вроде этого:
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);
}
}
Ответ 17
После сравнения вариантов ответов, представленных здесь, с возможностями F #, я обнаружил, что в F # улучшена поддержка переключения на основе типов (хотя я все еще придерживаюсь С#).
Возможно, вы захотите увидеть здесь и здесь.
Ответ 18
Создайте интерфейс 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 приведения, так что это дороже.
Ответ 19
Согласно спецификации С# 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
.
Сравнивая это с 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.
Ответ 20
Вы ищете Discriminated Unions
, которые являются языковой функцией F #, но вы можете добиться аналогичного эффекта, используя библиотеку, которую я создал, называемую OneOf
https://github.com/mcintyre321/OneOf
Основным преимуществом над switch
(и if
и exceptions as control flow
) является то, что он безопасен во время компиляции - нет обработчика по умолчанию или проваливается
void Foo(OneOf<A, B> o)
{
o.Switch(
a => a.Hop(),
b => b.Skip()
);
}
Если вы добавите третий элемент в o, вы получите ошибку компилятора, поскольку вам нужно добавить обработчик Func внутри вызова коммутатора.
Вы также можете сделать .Match
, который возвращает значение, а не выполняет оператор:
double Area(OneOf<Square, Circle> o)
{
return o.Match(
square => square.Length * square.Length,
circle => Math.PI * circle.Radius * circle.Radius
);
}
Ответ 21
Я бы создал интерфейс с любым именем и именем метода, которое имело бы смысл для вашего коммутатора, позвольте их вызывать соответственно: 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();
// ...
}
По крайней мере, с этим вы в безопасности во время компиляции, и я подозреваю, что с точки зрения производительности это лучше, чем проверка типа во время выполнения.
Ответ 22
Я согласен с Джоном в том, что у меня есть хеш действий для имени класса. Если вы сохраните свой шаблон, вам может потребоваться использовать вместо него конструкцию "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("...");
Разница в том, что при использовании patter if (foo - Bar) {((Bar) foo).Action(); } вы делаете тип casting дважды. Теперь, возможно, компилятор будет оптимизировать и выполнять эту работу только один раз, но я не стал бы рассчитывать на это.
Ответ 23
Как говорит Пабло, подход к интерфейсу почти всегда подходит для этого. Чтобы действительно использовать коммутатор, другой альтернативой является создание пользовательского перечисления, обозначающего ваш тип в ваших классах.
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 ......
}
}
Ответ 24
Это альтернативный ответ, который смешивает вклады от ответов JaredPar и VirtLink со следующими ограничениями:
- Конструкция коммутатора ведет себя как функция и принимает функции как параметры к случаям.
- Обеспечивает правильную сборку и всегда существует функция по умолчанию.
- возвращается после первого соответствия (true для ответа 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;
}
}
Ответ 25
Да - просто используйте слегка странное название "сопоставление с образцом" из С# 7 и выше, чтобы сопоставить класс или структуру:
IObject concrete1 = new ObjectImplementation1();
IObject concrete2 = new ObjectImplementation2();
switch (concrete1)
{
case ObjectImplementation1 c1: return "type 1";
case ObjectImplementation2 c2: return "type 2";
}
Ответ 26
я использую
public T Store<T>()
{
Type t = typeof(T);
if (t == typeof(CategoryDataStore))
return (T)DependencyService.Get<IDataStore<ItemCategory>>();
else
return default(T);
}
Ответ 27
Должен работать с
тип дела _:
лайк:
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;
}
Ответ 28
Если вы знаете ожидаемый класс, но у вас все еще нет объекта, вы даже можете сделать это:
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";
}
}