应该何时尝试消除switch语句?

时间:2009-07-01 23:45:53

标签: c# refactoring switch-statement

我在我正在处理的代码库中遇到了一个switch语句,我试图找出如何用更好的代码替换它switch statements are considered a code smell。但是,阅读有关several replacing switch的stackoverflow上的statements帖子,我似乎无法想到替换此特定switch语句的有效方法。

它让我想知道这个特定的switch语句是否正常,以及是否有特殊情况认为switch语句是合适的。

在我的情况下,我正在努力解决的代码(稍微模糊不清)是这样的:

private MyType DoSomething(IDataRecord reader)
{
    var p = new MyType
                {
                   Id = (int)reader[idIndex],
                   Name = (string)reader[nameIndex]
                }

    switch ((string) reader[discountTypeIndex])
    {
        case "A":
            p.DiscountType = DiscountType.Discountable;
            break;
        case "B":
            p.DiscountType = DiscountType.Loss;
            break;
        case "O":
            p.DiscountType = DiscountType.Other;
            break;
    }

    return p;
}

有人可以建议一种方法来消除这种转变吗?或者这是一个合适的开关?如果是,那么switch语句还有其他适当的用途吗?我真的很想知道它们的适用位置,所以我不会浪费太多时间来消除我遇到的每一个开关语句,因为它们在某些情况下被认为是气味。

更新:根据Michael的建议,我做了一些搜索此逻辑的重复,发现有人在另一个类中创建了逻辑,有效地使整个switch语句变得多余。所以在这个特定代码的上下文中,switch语句是不必要的。但是,我的问题更多的是关于代码中switch语句的适当性以及我们是否应该总是在它们被发现时尝试替换它们,所以在这种情况下我倾向于接受这个switch语句是合适的答案。

13 个答案:

答案 0 :(得分:15)

这适用于switch语句,因为它使选择可读,并且易于添加或减去。

See this link.

答案 1 :(得分:14)

切换语句(特别是长语句)被认为是错误的,不是因为它们是switch语句,而是因为它们的存在表明需要重构。

switch语句的问题是它们在代码中创建了一个分叉(就像if语句一样)。每个分支都必须单独测试,并且每个分支内的每个分支都是......好吧,你明白了。

尽管如此,以下文章对使用switch语句有一些好的做法:

http://elegantcode.com/2009/01/10/refactoring-a-switch-statement/

对于您的代码,上面链接中的文章表明,如果您正在执行从一个枚举到另一个枚举的此类转换,您应该将您的开关放在自己的方法中,并使用return语句而不是休息声明。我以前做过这个,代码看起来更清晰:

private DiscountType GetDiscountType(string discount)
{
    switch (discount)
    {
        case "A": return DiscountType.Discountable;
        case "B": return DiscountType.Loss;
        case "O": return DiscountType.Other;
    }
}

答案 2 :(得分:3)

我认为为了更改代码而更改代码并不是最好用的时间。更改代码以使其更具可读性,更快速,更高效等等。不要仅仅因为某人说你做的事情“改变”而改变它。

-Rick

答案 3 :(得分:2)

这个开关声明很好。你们有没有其他的错误要注意?大声笑

然而,我注意到有一件事......你不应该在IReader []对象索引器上使用索引序数......如果列顺序改变了怎么办?尝试使用字段名称,即reader [“id”]和reader [“name”]

答案 4 :(得分:2)

在我看来,这不是开关陈述的气味,而是它们里面的东西。对我来说,这个switch语句没问题,直到它开始添加几个案例。那么可能值得创建一个查找表:

private static Dictionary<string, DiscountType> DiscountTypeLookup = 
  new Dictionary<string, DiscountType>(StringComparer.Ordinal)
    {
      {"A", DiscountType.Discountable},
      {"B", DiscountType.Loss},
      {"O", DiscountType.Other},
    };

根据您的观点,这可能或多或少可读。

如果你的案件的内容超过一两行,那么事情开始变得臭。

答案 5 :(得分:2)

Robert Harvey和Talljoe提供了很好的答案 - 你在这里有一个从字符代码到枚举值的映射。这最好表示为映射,其中映射的详细信息在一个地方提供,可以在地图中提供(如Talljoe建议的),也可以在使用switch语句的函数中提供(如Robert Harvey建议的那样)。

在这种情况下,这两种技术都可能很好,但我想提请你注意一个在这里或其他类似情况下可能有用的设计原理。 开放/已结算校长

如果映射可能会随着时间的推移而改变,或者可能是扩展的运行时(例如,通过插件系统或通过从数据库中读取映射的部分),那么使用注册表模式将帮助您遵循开放/关闭主体,实际上允许扩展映射而不影响使用映射的任何代码(正如他们所说 - 打开扩展,关闭以进行修改)。

我认为这是关于注册表模式的一篇很好的文章 - 看看注册表如何保存从某个键到某个值的映射?以这种方式,它类似于表示为switch语句的映射。当然,在你的情况下,你不会注册所有实现通用接口的对象,但你应该得到要点:

所以,为了回答原始问题 - case语句是不好的形式,因为我希望在你的应用程序的多个地方需要从字符代码到枚举值的映射,所以它应该被考虑在内。我引用的两个答案为您提供了很好的建议 - 如何做到这一点 - 根据您的喜好选择。但是,如果映射可能会随着时间的推移而发生变化,请将注册表模式视为使代码免受此类更改影响的一种方式。

答案 6 :(得分:1)

我不会使用ifif不如switch明确。 switch告诉我你在整个过程中都在比较同样的东西。

为了吓唬别人,这不如你的代码那么清晰:

if (string) reader[discountTypeIndex]) == "A")
   p.DiscountType = DiscountType.Discountable;
else if (string) reader[discountTypeIndex]) == "B")
   p.DiscountType = DiscountType.Loss;
else if (string) reader[discountTypeIndex]) == "O")
   p.DiscountType = DiscountType.Other;

这个switch可能没问题,您可以查看@Talljoe建议。

答案 7 :(得分:1)

折扣类型的开关是否位于整个代码中?添加新的折扣类型是否需要您修改几个这样的开关?如果是这样,你应该考虑将开关考虑在内。如果没有,在这里使用开关应该是安全的。

如果整个程序中存在大量折扣特定行为,您可能希望重构:

p.Discount = DiscountFactory.Create(reader[discountTypeIndex]);

然后折扣对象包含与计算折扣相关的所有属性和方法。

答案 8 :(得分:1)

是的,这看起来像是对switch语句的正确用法。

但是,我还有另一个问题。

为什么没有包含默认标签?在默认标签中抛出异常将确保在添加新的discountTypeIndex时忘记修改代码时程序将无法正常运行。

另外,如果要将字符串值映射到枚举,可以使用属性和反射。

类似的东西:

public enum DiscountType
{
    None,

    [Description("A")]
    Discountable,

    [Description("B")]
    Loss,

    [Description("O")]
    Other
}

public GetDiscountType(string discountTypeIndex)
{
    foreach(DiscountType type in Enum.GetValues(typeof(DiscountType))
    {
        //Implementing GetDescription should be easy. Search on Google.
        if(string.compare(discountTypeIndex, GetDescription(type))==0)
            return type;
    }

    throw new ArgumentException("DiscountTypeIndex " + discountTypeIndex + " is not valid.");
}

答案 9 :(得分:1)

您怀疑此switch语句是正确的:任何依赖于某种类型的switch语句都可能表示缺少多态(或缺少子类)。

然而,TallJoe的字典是一种很好的方法。

请注意如果你的枚举和数据库值是整数而不是字符串,或者如果你的数据库值与枚举名相同,那么反射就可以了,例如给定

public enum DiscountType : int
{
    Unknown = 0,
    Discountable = 1,
    Loss = 2,
    Other = 3
}

然后

p.DiscountType = Enum.Parse(typeof(DiscountType), 
    (string)reader[discountTypeIndex]));

就足够了。

答案 10 :(得分:0)

我认为这取决于您是否正在创建MType添加许多不同的地方或仅在此处。如果你在许多地方创建MType总是不得不切换dicsount类型有一些其他检查,那么这可能是代码气味。

我会尝试在你的程序中的一个单独的位置创建MTypes可能在MType本身的构造函数中或在某种工厂方法中但是程序的随机部分赋值可能导致某人不知道如何价值观应该是做错事。

所以开关很好但可能需要在Type

的创建部分内更多地移动开关

答案 11 :(得分:0)

我并不是绝对反对switch语句,但是在你出现的情况下,我至少已经消除了分配DiscountType的重复;我可能会改为编写一个返回给定字符串的DiscountType的函数。该函数可以简单地为每个案例提供返回语句,从而无需中断。我发现需要在开关盒之间断开非常危险。

private MyType DoSomething(IDataRecord reader)
{
    var p = new MyType
                {
                   Id = (int)reader[idIndex],
                   Name = (string)reader[nameIndex]
                }

    p.DiscountType = FindDiscountType(reader[discountTypeIndex]);

    return p;
}

private DiscountType FindDiscountType (string key) {
    switch ((string) reader[discountTypeIndex])
    {
        case "A":
            return DiscountType.Discountable;
        case "B":
            return DiscountType.Loss;
        case "O":
            return DiscountType.Other;
    }
    // handle the default case as appropriate
}

很快,我注意到FindDiscountType()确实属于DiscountType类并移动了函数。

答案 12 :(得分:-1)

当您设计一种语言并最终有机会删除整个语言中最丑陋,最直观的错误倾向语法。

当您尝试删除switch语句时。

为了清楚起见,我指的是语法。这是从C / C ++中获取的东西,它应该被改变以符合C#中更现代的语法。我完全同意提供开关的概念,因此编译器可以优化跳转。