有没有比这更好的选择'开关类型'?

鉴于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());
}
}
150480 次浏览

一种选择是有一个从TypeAction(或其他一些委托)的字典。根据类型查找操作,然后执行它。我以前在工厂用过这个。

创建一个超类(S),并使a和B从它继承。然后在S上声明一个每个子类都需要实现的抽象方法。

这样做,"foo"方法还可以将其签名更改为foo (S o),使其类型安全,并且不需要抛出那个丑陋的异常。

使用c# 7随Visual Studio 2017 (Release 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()操作符中使用switch语句(谢谢@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;
}

我也会

在这种情况下,我通常以谓词和操作的列表结束。大致如下:

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

你应该重载你的方法,而不是尝试自己去消除歧义。到目前为止,大多数答案都没有考虑到未来的子类,这可能会导致以后真正可怕的维护问题。

在比较了这里提供的一些f#特性的选项后,我发现f#对基于类型的切换有更好的支持(尽管我仍然坚持使用c#) 你可能想看到在这里在这里.

我同意Jon关于类名的操作散列。如果你想保留你的模式,你可能想要考虑使用"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();}你做了两次类型转换。现在编译器可能会优化,只做一次工作-但我不会指望它。

创建一个接口IFooable,然后让你的AB类实现一个通用方法,该方法反过来调用你想要的相应方法:

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,然后再强制转换,因为这样会进行两次强制转换,所以代价更大。

在c# (更新:在c# 7 / VS 2017中支持切换类型- 参见Zachary Yates的回答)中绝对缺乏对类型的切换。为了不使用大量的if/else if/else语句,您需要使用不同的结构。前段时间我写了一篇博文,详细介绍了如何构建TypeSwitch结构。

https://learn.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
};
}
}
另一种方法是定义一个接口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();
}
}

如果您正在使用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

{
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 Tfoo.GetType() == typeof(T)不一样!!

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

正如Pablo所建议的,界面方法几乎总是处理这个问题的正确方法。要真正利用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 ......
}
}

你可以创建重载方法:

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

我喜欢Virtlink的隐式类型的使用使切换更具可读性,但我不喜欢early-out是不可能的,而且我们正在进行分配。让我们把性能提高一点。

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

可读且快速。现在,正如每个人在他们的答案中不断指出的那样,考虑到这个问题的性质,顺序在类型匹配中很重要。因此:

  • 先放叶类型,再放基类型。
  • 对于对等类型,将可能性更大的匹配放在前面,以最大化性能。
  • 这意味着不需要特殊的默认情况。相反,只需使用lambda中的base-most类型,并将其放在最后。

对于内置类型,可以使用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;
}

这是一个混合来自JaredPar和VirtLink答案的备选答案,具有以下约束:

  • 开关构造表现为函数,并接收函数作为参数到case。
  • 确保它被正确构建,并且始终存在默认的功能
  • 第一场比赛后返回(对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;
}
}

是的,感谢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());
}

你正在寻找Discriminated Unions,这是f#的一个语言特性,但你可以通过使用我做的一个名为OneOf的库来实现类似的效果

https://github.com/mcintyre321/OneOf

switch(以及ifexceptions as control flow)相比的主要优点是它是编译时安全的——没有默认处理程序或漏洞

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

如果你在o中添加第三个项,你会得到一个编译器错误,因为你必须在switch调用中添加一个处理程序Func。

你也可以执行.Match,它返回一个值,而不是执行语句:

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

根据c# 7.0规范,你可以在switchcase中声明局部变量作用域:

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语句链一样快(如果不是更快的话):

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.

是的——只需要使用c# 7中命名有点奇怪的“模式匹配”来匹配类或结构:

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


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

我将创建一个接口,其名称和方法名称对您的交换机有意义,让我们分别调用它们: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();
// ...
}

至少这样你在编译时是安全的,而且我认为在性能方面它比在运行时检查类型要好。

我使用

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


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

你可以在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;
}

应该与

case type _:

如:

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

如果你知道你期待的类,但你仍然没有一个对象,你甚至可以这样做:

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

c# 8对模式匹配的增强使得这样做成为可能。在某些情况下,它做到了,而且更简洁。

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

在c# 8之后,你可以使用新的开关使它更加简洁。通过使用discard选项_,你可以避免在你不需要的时候创建不必要的变量,像这样:

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

Invoice和ShippingList是类,document是一个对象,可以是它们中的任何一个。

试着这样做:

public void Test(BaseType @base)
{
switch (@base)
{
case ConcreteType concrete:
DoSomething(concrete);
break;


case AnotherConcrete concrete:
DoSomething(concrete);
break;
}
}