Is there a better alternative than this to 'switch on type'?
因为C不能打开一个类型(我收集到它不是作为特殊情况添加的,因为IS-A关系意味着可能会应用多个不同的情况),有没有比这更好的方法来模拟打开类型?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
打开类型在C中显然是缺乏的(更新:在C 7/vs 2017中,支持打开类型-请参阅下面的Zachary Yates的答案)。为了在不使用大型if/else if/else语句的情况下完成此操作,需要使用不同的结构。我写了一篇博文,详细介绍了如何构建一个类型转换结构。
http://blogs.msdn.com/jaredpar/archive/2008/05/16/switching-on-types.aspx
简短版本:typeswitch的设计目的是防止多余的强制转换,并提供类似于普通switch/case语句的语法。例如,这里是标准Windows窗体事件上的类型切换操作
1 2 3 4 5 | 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")); |
类型转换的代码实际上非常小,可以很容易地放入您的项目中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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 }; } } |
使用随Visual Studio 2017(15.*版)提供的C 7,您可以在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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语句(谢谢@joey adams):
对于C 5和更早的版本,您可以使用switch语句,但是您必须使用包含类型名的神奇字符串…这对重构不是特别友好(谢谢@nukefusion)
1 2 3 4 | switch(o.GetType().Name) { case"AType": break; } |
一种选择是拥有从
Jaredpar的答案在我的脑后,我写了他的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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; } |
注意,
获取我的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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); } } } |
创建一个超类,并使a和b从中继承。然后在S上声明每个子类都需要实现的抽象方法。
这样做,"foo"方法还可以将其签名更改为foo(s o),使其类型安全,并且您不需要抛出那个丑陋的异常。
如果您使用的是C 4,那么您可以利用新的动态功能来实现一个有趣的替代方案。我不是说这更好,事实上,它看起来很可能会慢一些,但它确实有一定的优雅。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Thing { void Foo(A a) { a.Hop(); } void Foo(B b) { b.Skip(); } } |
用法:
1 2 3 | object aOrB = Get_AOrB(); Thing t = GetThing(); ((dynamic)t).Foo(aorB); |
这样做的原因是C 4动态方法调用在运行时而不是编译时解决了它的重载。我最近写了更多关于这个想法的文章。我再次重申,这可能比所有其他建议的效果更差,我只是出于好奇。
您真的应该重载您的方法,而不是自己尝试去消除歧义。到目前为止,大多数答案都没有考虑到未来的子类,这可能会导致以后的维护问题。
对于内置类型,可以使用类型代码枚举。请注意,getType()有点慢,但在大多数情况下可能不相关。
1 2 3 4 5 6 7 8 9 | switch (Type.GetTypeCode(someObject.GetType())) { case TypeCode.Boolean: break; case TypeCode.Byte: break; case TypeCode.Char: break; } |
对于自定义类型,可以创建自己的枚举,以及带有抽象属性或方法的接口或基类…
属性的抽象类实现
1 2 3 4 5 6 7 8 9 | 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; } } } |
方法的抽象类实现
1 2 3 4 5 6 7 8 9 | 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; } } |
属性的接口实现
1 2 3 4 5 6 7 8 9 | public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu }; public interface IFooType { FooTypes FooType { get; } } public class FooFighter : IFooType { public FooTypes FooType { get { return FooTypes.FooFighter; } } } |
方法的接口实现
1 2 3 4 5 6 7 8 9 | public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu }; public interface IFooType { FooTypes GetFooType(); } public class FooFighter : IFooType { public FooTypes GetFooType() { return FooTypes.FooFighter; } } |
我的一个同事也告诉过我:这有一个优点,你可以把它用于任何类型的物体,而不仅仅是你定义的物体。它的缺点是有点大和慢。
首先定义这样的静态类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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()); } } } |
然后你可以这样使用它:
1 2 3 4 5 6 7 | switch (TypeEnumerator.EnumerateType(someObject)) { case TypeEnumerator.TypeEnumeratorTypes._int: break; case TypeEnumerator.TypeEnumeratorTypes._string: break; } |
我喜欢virtlink使用隐式类型来提高开关的可读性,但我不喜欢这样的做法:不可能提前退出,而且我们正在进行分配。让我们把表演调高一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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中进行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <#@ 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的例子:
1 2 3 4 5 6 7 | 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中最基本的类型,并将其放在最后。
考虑到继承有助于将对象识别为多个类型,我认为切换可能会导致严重的歧义。例如:
案例1
案例2
因为S是一个字符串和一个对象。我认为,当你写一个
可以考虑使用编译器检查"typeswitch"语句的类型,检查枚举的类型是否互相继承。但这并不存在。
是的,感谢C 7,这是可以实现的,以下是它的实现方式(使用表达式模式):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
使用C 7和模式匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
我也可以
- 使用方法重载(就像X0n),或者
- 使用子类(就像pablo),或者
- 应用访客模式。
我在这里看了几个选项,反映了F可以做什么。f对基于类型的切换有更好的支持(尽管我仍然坚持使用c-p)。你可能想看看这里和这里。
另一种方法是定义一个接口,然后在两个类中实现它。这是狙击手:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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(); } } |
创建一个接口ifooable,然后使A和B类实现一个公共方法,该方法反过来调用您想要的相应方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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",然后使用casting,因为这样可以制作2个casts(昂贵)。
可以创建重载方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void Foo(A a) { a.Hop(); } void Foo(B b) { b.Skip(); } void Foo(object o) { throw new ArgumentException("Unexpected type:" + o.GetType()); } |
并使用动态参数类型绕过静态类型检查:
1 | Foo((dynamic)something); |
在这种情况下,我通常会得到一个谓词和动作的列表。沿着这些线的东西:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class Mine { static List<Func<object, bool>> predicates; static List<Action<object>> actions; static Mine() { AddAction<A>(o => o.Hop()); AddAction(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); } } |
我将创建一个接口,不管名称和方法名对您的交换机有什么意义,让我们分别调用它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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(); } } |
改变方法如下:
1 2 3 4 5 6 7 | void Foo<T>(T obj) where T : IDoable { // ... obj.Do(); // ... } |
至少在编译时您是安全的,我怀疑从性能上讲,它比在运行时检查类型要好。
根据C_7.0规范,您可以声明一个在
1 2 3 4 5 6 7 8 9 10 11 12 13 | object a ="Hello world"; switch (a) { case string _: // The variable 'a' is a string! break; case int _: // The variable 'a' is an int! break; case Foo _: // The variable 'a' is of type Foo! break; } |
你是不是在问为什么变量被声明为
好吧,C 7.0中引入的另一个特性是,您可以将其命名为从未引用过的变量。所以,不能引用变量
这是实现这一点的最佳方法,因为它只涉及强制转换和推送堆栈操作,这是解释器在执行位操作和
与
因此,据我所知,我不认为存在一种更快的方法,即使你不想使用
1 2 3 4 5 6 7 8 |
你要找的是
网址:https://github.com/mcintyre321/oneof
与
1 2 3 4 5 6 7 | void Foo(OneOf<A, B> o) { o.Switch( a => a.Hop(), b => b.Skip() ); } |
如果向o中添加第三个项,将得到一个编译器错误,因为必须在switch调用中添加一个处理程序func。
您还可以执行返回值而不是执行语句的
1 2 3 4 5 6 7 | double Area(OneOf<Square, Circle> o) { return o.Match( square => square.Length * square.Length, circle => Math.PI * circle.Radius * circle.Radius ); } |
这是一个备选答案,它混合了来自jaredpar和virtlink答案的贡献,并具有以下限制:
- 开关结构作为一个函数,接收作为事例参数的函数。
- 确保它是正确构建的,并且始终存在一个默认函数。
- 它在第一次匹配后返回(对于jaredpar答案为true,对于virtlink one则不是true)。
用途:
1 2 3 4 5 6 | var result = TSwitch<string> .On(val) .Case((string x) =>"is a string") .Case((long x) =>"is a long") .Default(_ =>"what is it?"); |
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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; } } |
正如Pablo所说,接口方法几乎总是处理这一问题的正确方法。要真正使用switch,另一种选择是使用一个自定义枚举来表示类中的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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,另一个例子是用于基元类型的
1 2 3 4 5 6 7 8 9 | void Foo(object o) { switch (Type.GetTypeCode(o.GetType())) // for IConvertible, just o.GetTypeCode() { case TypeCode.Int16: case TypeCode.Int32: //etc ...... } } |
是的-只需使用从C 7向上略显奇怪的"模式匹配"来匹配类或结构:
1 2 3 4 5 6 7 8 |
我用
1 2 3 4 5 6 7 8 9 |
如果您知道您期望的类,但您仍然没有对象,那么您甚至可以这样做:
1 2 3 4 5 6 7 8 9 10 |
应该与
case type _:
像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; } |
我同意乔恩对类名的操作进行散列。如果保留您的模式,您可能会考虑改用"as"构造:
1 2 3 4 5 6 7 8 9 10 11 | 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();时,您要执行两次类型转换。现在,也许编译器会进行优化,只做一次——但我不会指望它。