网上有很多关于switch-case语句的争论。似乎有一半的程序员认为switch-case语句实际上是一种反模式,而另一半程序员则声称这一概念实际上是有用途的。通常,第二组人试图证明一个观点:在一些简单的情况下,使用switch-case是可以的,比如一些非常简单的检查。我的经验告诉我另一种情况,我完全属于第一组。
1.switch-case语句的问题
在这些所谓的 "简单情况 "下,这些语句往往会失控,我们通常会得到一大块无法阅读的代码。更不用说如果需求发生变化(我们知道它们会发生变化),语句本身就必须修改,从而破坏了开放-封闭原则。这种情况在企业系统中尤其明显,这种想法会导致维护地狱。
switch-case语句(就像if-else语句)的另一个问题是,它们将数据和行为结合在一起,这让我们想起了程序化编程。这意味着什么,为什么它是坏的?好吧,数据的本质是不包含行为的信息。 将数据视为行为,并将其用于控制你的应用程序的工作流程,是创造所提到的维护地狱的原因。例如,看一下这段代码:
string data = "Item1";
var action1 = new Action(() => { Console.Write("This is one!"); });
var action2 = new Action(() => { Console.Write("This is two!"); });
var action3 = new Action(() => { Console.Write("This is three!"); });
switch (data)
{
case "Item1":
action1();
break;
case "Item2":
action2();
break;
default:
action3();
break;
}
这段代码中的数据是名为data的变量,但同时数据也是打印在控制台的字符串。行为是在控制台打印信息的动作,同时行为也是将数据集中的信息集映射到一些动作上,即将 "项目1 "映射到动作1,"项目2 "映射到动作2,等等。

正如你所看到的,数据是可以被改变的,而行为是不应该被改变的,把它们混在一起就是问题的根源。
那么,如何摆脱switch-case语句的困扰呢?
2.从switch-case到面向对象的代码
最近,我正在处理依赖switch-case语句的代码。所以,我根据这个现实世界的问题和我重构它的方式创建了下一个例子:
public class Entity
{
public string Type { get; set; }
public int GetNewValueBasedOnType(int newValue)
{
int returnValue;
switch (Type)
{
case "Type0":
returnValue = newValue;
break;
case "Type1":
returnValue = newValue * 2;
break;
case "Type2":
returnValue = newValue * 3;
break;
}
return newValue;
}
}
在这里,我们有一个带有属性Type的实体类。这个属性定义了函数GetNewValueBasedOnType将如何计算其结果。这个类是以这种方式使用的:
var entity = new Entity() { Type = "Type1" };
var value = entity.GetNewValueBasedOnType(6);

为了将这个例子修改成适当的面向对象的代码,我们需要改变相当多的东西。我们可以注意到的第一件事是,即使我们有多个实体类型,它们仍然只是 实体 类型。一个甜甜圈就是一个甜甜圈,不管它是什么味道。这意味着我们可以为每个实体类型创建一个类,而所有这些类都应该实现一个抽象类。另外,我们还可以为实体类型定义一个枚举。这看起来像这样:
public enum EntityType
{
Type0 = 0,
Type1 = 1,
Type2 = 2
}
public abstract class Entity
{
public abstract int GetNewValue(int newValue);
}
实体类的具体实现看起来是这样的:
public class Type0Entity : Entity
{
public override int GetNewValue(int newValue)
{
return newValue;
}
}
public class Type1Entity : Entity
{
public override int GetNewValue(int newValue)
{
return 2*newValue;
}
}
public class Type2Entity : Entity
{
public override int GetNewValue(int newValue)
{
return 3*newValue;
}
}
这就好多了。现在我们更接近于真正的面向对象的实现,而不仅仅是一些花哨的程序性实现。我们使用了所有那些面向对象编程为我们提供的好概念,比如抽象和继承。

不过,还是有一个问题,那就是如何使用这些类。似乎我们并没有删除switch-case语句,我们只是把它从Entity类移到了我们将创建具体Entity实现的对象的地方。基本上,我们仍然需要确定我们需要实例化哪个类。在这一点上,我们可以制作Entity Factory。
public class EntityFactory
{
private Dictionary<EntityType, Func<entity>> _entityTypeMapper;
public EntityFactory()
{
_entityTypeMapper = new Dictionary<entitytype, func<entity="">>();
_entityTypeMapper.Add(EntityType.Type0, () => { return new Type0Entity(); });
_entityTypeMapper.Add(EntityType.Type1, () => { return new Type1Entity(); });
_entityTypeMapper.Add(EntityType.Type2, () => { return new Type2Entity(); });
}
public Entity GetEntityBasedOnType(EntityType entityType)
{
return _entityTypeMapper[entityType]();
}
}
现在,我们可以像这样使用这段代码:
try
{
Console.WriteLine("Enter entity type:");
var entytyType = (EntityType)Enum.Parse(typeof(EntityType), Console.ReadLine(), true);
Console.WriteLine("Enter new value:");
var modificationValue = Convert.ToInt32(Console.ReadLine());
var entityFactory = new EntityFactory();
var entity = entityFactory.GetEntityBasedOnType(entytyType);
var result = entity.GetNewValue(modificationValue);
Console.WriteLine(result);
Console.ReadLine();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
很多时候,我把这段代码给别人看,会得到一个评论说这太复杂了。但是,问题是,这才是真正的面向对象的代码应该有的样子。它对变化更有弹性,其行为与数据分离。数据不再控制工作流程。我们可以看到,实体类型和输入值是可以改变的,但它们不受行为的干扰。通过这样做,我们提高了代码的可维护性。这段代码的下一个进化步骤将是使用策略模式。
我通常得到的另一个抱怨是,这段代码仍然没有完全满足Open-Close原则,也就是说,如果需要添加一个新的实体类型,我们需要改变实体工厂类本身。这是一个非常好的抱怨,而且说得很有道理。尽管如此,我们不应该忘记,满足SOLID原则的代码是我们应该一直努力的,但很多时候我们不能完全达到这个目的。在我看来,Open-Close原则是所有SOLID原则中最难以达到的。
3.C#中的模式匹配--演变
你可能会问自己为什么这一章会在这里。我们正在讨论switch-case语句,那么模式匹配与此有什么关系?好吧,记得我说过,我认为不应该使用switch-case吗?事情是这样的,几年前,C# 7引入了模式匹配功能。这个功能在函数式编程语言中已经很成熟了,比如F#,它在很大程度上依赖于switch-case语句,所以让我们花点时间再深入了解一下,看看这是否能改变我的想法。
模式匹配毕竟是什么?为了更好地解释这个问题,我将使用F#的例子。F#有一个强大的概念,叫做判别联合。使用这个概念,我们可以定义一个变量,它可以有多种类型,这意味着不仅值是可以改变的,而且类型也是可以改变的。等等,什么?这里有一个例子:
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float

这意味着我们有一个定义的形状类型,它可以是一个矩形或一个圆形。shape类型的变量可以是这两种类型中的任何一种。就像薛定谔的猫一样,在我们仔细观察之前,我们不会知道该变量的类型。现在,让我们假设我们想写一个函数,以获得形状的高度。这看起来会是这样的:
let getShapeHeight shape =
match shape with
| Rectangle(width = h) -> h
| Circle(radius = r) -> 2. * r
这里发生的事情是,我们检查了变量shape的类型,如果形状是矩形类型,则返回矩形宽度,如果形状是圆形类型,则返回两倍的半径值。我们已经打开了盒子,折叠了一个波函数。
虽然这个概念起初看起来不必要地复杂,但实际上它既优雅又强大。控制工作流给我们带来了很多可能性,所以在当年,当我看到它将成为C#的一部分时,我非常高兴。 那么在改进之前,这段代码在C#中是怎样的呢?
var shapes = new Dictionary<string, object>
{
{ "FirstShape", new Rectangle(1, 1) },
{ "SecondShape", new Circle(6) }
};
foreach(var shape in shapes)
{
if (shape.Value is Rectangle)
{
var height = ((Rectangle)shape.Value).Height;
Console.WriteLine(height);
}
else if (shape.Value is Circle)
{
var height = ((Circle)shape.Value).Radius * 2;
Console.WriteLine(height);
}
}
3.1 C# 7中的模式匹配
这个功能在C#中给我们带来的是简化这个语法的能力。它也为switch语句提供了更多的可用性,也就是说,现在我们可以通过变量的类型进行切换。
foreach (var shape in shapes)
{
switch(shape.Value)
{
case Rectangle r:
Console.WriteLine(r.Height);
break;
case Circle c:
Console.WriteLine(2 * c.Radius);
break;
}
}
现在可以做的另一件事是使用when子句。
foreach (var shape in shapes)
{
switch(shape.Value)
{
case Rectangle r:
Console.WriteLine(r.Height);
break;
case Circle c when c.Radius > 10:
Console.WriteLine(2 * c.Radius);
break;
}
}
这意味着我们可以在case条件下检查对象的特定属性。现在,随着C#新版本的出现,这个功能确实得到了改善,所以这只是模式匹配的一个开始。让我们来看看。

3.2 C# 8中的模式匹配
在C#8中,我们得到了switch-case表达式。这个功能旨在进一步提高开关大小写的可用性,并且与模式匹配配合得非常好。我的意思是,真的很好。这两个功能一起给了我们写这样的代码的能力。
var message = shape switch
{
Circle cir => "This is a circle with area {cir.Area}!",
Rectange rec when rec.height==rec.width => "This is a square!",
{ Area : 111 } => "Area is 111",
_ => "This is a shape!"
}
所以,通过这种方法,我们有更多的控制权。我们可以检查类型,特定类型的属性值,基本类型的属性值,等等。然而,请注意,我们不能说*{Area > 111},*我们必须真正具体地说明这个值。所以,这是一个很大的进步,但仍然没有达到目的。
3.3 C# 9中的模式匹配
C# 9正好改善了我在上一节中提到的问题。然而,这并不是唯一的改进。微软的人听取了社区的反馈,改进了模式匹配,所以我们可以做到这一点:
var message = shape switch
{
Circle cir => "This is a circle with area {cir.Area}!",
Rectangle rec when rec.height == rec.width => "This is a square!",
Circle cir { Area : > 111 and < 222} => "This is a Circle with specific area value",
{ Area == 111 } => "Area is 111",
_ => "This is a shape!"
}
var areaMessage = shape.Area switch
{
> 111 and < 222 => "This is a specific area",
_ => "Nothing specific about the area"
}
这就是很多事情的关键所在。你可以使用像 "和 "和 "不 "这样的运算符来匹配特定的模式。至少在我眼里,这是模式匹配和切换大小写的一个重大飞跃。

3.4 C# 10中的模式匹配
在C#10中,微软继续改进模式匹配,让我们有能力检查对象中的对象。例如,如果我们像这样扩展Shape类。
public abstract class Shape
{
public abstract double Area { get; }
public Shape ShapeWithinShape { get; set; }
}
那么,我们就添加了一个属性,代表形状内的形状。这可以像这样与模式匹配一起使用。
if (shape is Rectangle { ShapeWithinShape.Area == 111 })
{
// Do some amaizing stuf
}
3.5 C# 11中的模式匹配
在模式匹配方面,C# 11有一些改进。例如,在C# 11中,你可以 用常量字符串 匹配 Span 和 ReadOnlySpan 。另外,利用.NET技术,数据科学项目的工作变得更加容易,因为有了Lists模式匹配的功能。有趣的是,在以前的 Python 版本中也增加了类似的功能 。在这里了解更多关于C# 11的功能。
总结
说实话,现在我可以看到switch-case语句又爬回了我的代码中。当然,它是以表达式和模式匹配的形式出现的。我仍然坚决反对开箱即用的switch-case。然而,看到模式匹配多年来的发展,我的脑海中涌现出许多想法。
我在这篇文章中没有涉及的是策略模式,在某些情况下,如果你想从你的代码中删除switch-case,这也是一个不错的选择。