这个“switch on type”有更好的替代品吗?

410

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

6
见也: https://dev59.com/HnVC5IYBdhLWcg3w4VVz https://dev59.com/RWw05IYBdhLWcg3wahNN https://dev59.com/ZGsz5IYBdhLWcg3w9syr https://dev59.com/5G855IYBdhLWcg3wMRVW - Mikhail Poda
5
  1. 这是关于在C#中使用字符串类型或者elseif语句进行条件判断哪种方法更快的问题。
  2. 这是关于如何在运行时根据对象类型使用C# switch语句的问题。
  3. 这个问题探讨了为什么在C#中使用switch语句判断类型容易引起混淆。
  4. 这是有关如何在C#中切换对象可能的类型的问题。
  5. 这是关于基于类型切换行为的最佳方法的问题。
  6. 这是关于在C#中使用哪种最佳替代方案来替换switch语句的问题。
- Mikhail Poda
32个回答

438

使用在 Visual Studio 2017(版本 15.*)中发布的 C# 7.0,您可以在 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;
}

28
我不喜欢这个答案,因为 nameof(NamespaceA.ClassC) == nameof(NamespaceB.ClassC) 是成立的。 - ischas
12
(C# 7)如果您不需要访问对象,则还可以使用下划线:case UnauthorizedException _: - Assaf S.
1
@ischas,那是非常重要的一点。谢谢!然而,使用 nameofToString 更好,因为它具有编译器保护。 - 4thex

292

在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语句的语法。例如,以下是在标准Windows窗体事件上运行TypeSwitch的示例:

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)",这样可以考虑到兼容类型(例如子类)。 - Mark Cidade
修改了代码,使用“entry.Target.IsAssignableFrom(type)”以支持子类。 - Matt Howells
3
值得注意的一件事是(根据我的理解)需要将“默认”操作放在最后,以确保检查所有其他情况。我相信这不是标准开关中的要求 - 任何情况下我都没有看到有人尝试在其他位置设置“默认”。为此,有几个故障安全选项可供选择,可以排序数组以确保默认设置在最后(有些浪费),或者将默认设置为一个变量,在foreach之后进行处理(仅当找不到匹配时才会发生)。 - musefan
我最终一直使用这种方法,所以将其包装在一个项目 https://github.com/asgerhallas/ShinySwitch 中,并且还发布到了 nuget 上:https://www.nuget.org/packages/ShinySwitch/。 - asgerhallas

105

一个选择是拥有从TypeAction(或其他委托)的字典。根据类型查找动作,然后执行它。我之前曾将其用于工厂。


32
小注:适用于一对一的匹配,但在继承和/或接口方面可能会有些麻烦,特别是由于字典中不保证顺序被保留。但仍然,这是我在很多地方使用的方法;-p 所以+1。 说明:这种方法适合进行一对一的匹配,但在处理继承和接口时可能会有一些问题,尤其是因为字典中的顺序不能保证被保留。虽然如此,在许多地方我仍然采用了这种方法;-p 因此,我赞成这种方法。 - Marc Gravell
@Marc:在这种范例中,继承或接口会如何被打破?假设键是一种类型,动作是一种方法,那么据我所知,继承或接口实际上应该强制执行正确的操作。我当然了解多个操作和缺乏排序的问题。 - Harper Shelby
4
这种技术在继承和接口方面会出现问题,因为你需要在进行检查的对象和调用委托之间建立一对一的对应关系。在字典中应该寻找一个对象的哪个多个接口? - Robert Rossney
5
如果你要为这个目的构建一个字典,你可以重载索引器,以返回键类型的值,如果找不到,则返回其超类,如果没有,则返回其超类,以此类推,直到没有更多内容。 - Erik Forbes
所以,如果我有一个继承链,如:Object > Foo > Bar > Blee - 并且我为 'Foo' 定义了一个操作,则针对 'Blee' 或 'Bar' 的查找将找到指定为 'Foo' 的操作。 - Erik Forbes

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

@Virtlink 感谢您提供这个静态助手,我为了与 Silverlight(Windows Phone)兼容以及 this.value 只读,对您的要点进行了小幅修改。 - Cœur
2
你还可以为初始情况添加一个扩展方法:public static Switch<TSource> Case<TSource, TTarget>(this TSource value, Action<TTarget> action) where TTarget : TSource。这样,你就可以说 value.Case((C x) ... - Joey Adams
此外,我认为你可以简单地说“value is TTarget”,而不是所有的“IsAssignableFrom”东西。 - Joey Adams
1
@JoeyAdams:我采纳了您的最后建议,加上一些小的改进。但是,我保持原来的语法不变。 - Daniel A.A. Pelsmaeker
@Virtlink:很酷。我同意不使用Case扩展方法,这样任何遇到使用TypeSwitch的代码的人都能清楚地看到正在使用一个实用类。我想你可以创建一个与On具有相同签名的TypeSwitch扩展方法(这样你就可以说value.TypeSwitch().Case((C x) ...),但这并不是什么大问题。 - Joey Adams

22

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

谢谢您提供这个代码片段! 它也可以用于检测子类: if (this.TemplatedParent.GetType().IsSubclassOf(typeof(RadGridView))) 可以改为: switch (this.TemplatedParent.GetType()) case var subRadGridView when subRadGridView.IsSubclassOf(typeof(RadGridView)): - Flemming Bonde Kentved
你做错了。看看Serge Intern的答案,并阅读有关Liskov替换原则的内容。 - 0xF

18
C# 8模式匹配的增强使得可以像这样做。这种语法更加简洁。
public Animal Animal { get; set; }

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

1
如果未使用标识符,则可以省略它。但是,如果使用了标识符,则应将其作为顶部答案。 - burton
有没有办法在泛型参数T上做类似的事情? - Yiping

14

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

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


是的 Bruno,但问题没有暗示那一点。不过你可以在你的答案中包含那个信息 Pablo。 - Dana the Sane
从问题中,我认为A和B是足够通用的,例如A = String; B = List<int>... - bruno conde

11

是的,多亏了 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());
}

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动态方法调用会在运行时而不是编译时解决其重载。我最近写了更多关于这个想法的内容,链接在这里:http://paulbatum.blogspot.com/2008/11/no-visitors.html。再次强调,这种方法可能比所有其他建议的方法都要慢,我只是将其作为一种好奇心呈现出来,不建议使用。

1
我今天也有同样的想法。这个方法大概比直接使用类型名称慢三倍左右。当然,慢快取决于具体情况(对于6000万次调用,只需要4秒钟)。而且,代码更加易读,所以完全值得。 - Daryl

7
你应该重载你的方法,而不是试图自己进行消歧。迄今为止,大多数答案都没有考虑未来的子类,这可能会导致以后非常糟糕的维护问题。

3
重载决议是在静态确定的,所以那样做根本行不通。 - Neutrino
@Neutrino:问题中没有规定类型在编译时未知。如果已知,重载比任何其他选项都更有意义,考虑到OP的原始代码示例。 - Peter Duniho
我认为他试图使用“if”或“switch”语句来确定类型,这表明该类型在编译时是未知的。 - Neutrino
@Neutrino,我提醒你一下,正如Sergey Berezovskiy所指出的那样,C#中有一个动态关键字,它代表了一个需要在运行时动态解决的类型(而不是编译时)。 - Davide Cannizzo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接