if和else if的替代方案

15

我有很多if、else if语句,我知道必须有更好的方法来做到这一点,但即使在搜索stackoverflow后,我仍不确定如何在我的特定情况下实现。

我正在解析文本文件(账单),根据账单上出现的特定字符串将服务提供商的名称赋值给变量(txtvar.Provider)。

以下是我正在做的小样本(不要笑,我知道它很凌乱)。总共有大约300个if、else if的语句。

if (txtvar.BillText.IndexOf("SWGAS.COM") > -1)
{
    txtvar.Provider = "Southwest Gas";
}
else if (txtvar.BillText.IndexOf("georgiapower.com") > -1)
{
    txtvar.Provider = "Georgia Power";
}
else if (txtvar.BillText.IndexOf("City of Austin") > -1)
{
    txtvar.Provider = "City of Austin";
}
// And so forth for many different strings

我希望使用类似于 switch 语句的东西,以使代码更高效和易读,但我不确定如何比较 BillText。我正在寻找类似于下面这样的东西,但无法弄清楚如何使其起作用。

switch (txtvar.BillText)
{
    case txtvar.BillText.IndexOf("Southwest Gas") > -1:
        txtvar.Provider = "Southwest Gas";
        break;
    case txtvar.BillText.IndexOf("TexasGas.com") > -1:
        txtvar.Provider = "Texas Gas";
        break;
    case txtvar.BillText.IndexOf("Southern") > -1:
        txtvar.Provider = "Southern Power & Gas";
        break;
}

我肯定愿意接受各种想法。

但我需要有能力决定值的评估顺序。正如你所想象的那样,当解析成百上千种略有不同的布局时,我偶尔会遇到一个问题,即没有一个明显独特的指示符来确定账单属于哪个服务提供商。


2
一个针对字符串列表的for循环?编辑:针对BillText,Provider元组列表的for循环。 - millimoose
2
switch(true) 可以工作。 - Asad Saeeduddin
2
没有必要使用多个标签(特别是不适用的标签,如解析)。此外,为了说明概念,也不需要发布数十行冗余代码。我们在这里很快就能理解。 :-) - Ken White
4
您可以使用 txtvar.BillText.Contains("value") 来提高代码的可读性。 - Ben
1
这里的评论适用于几乎所有答案。使用字典(=数据)而不是if/else(=代码),使得从外部源(如csv文件)读取替换变得非常容易,而无需重新编译应用程序即可扩展。 - linac
显示剩余3条评论
8个回答

23

为什么不充分利用 C# 所提供的功能呢?下面这段使用了匿名类型、集合初始化器、隐式类型变量和 Lambda 语法的 LINQ 是简洁、直观的,同时也保持了对按顺序评估模式的修改要求:

var providerMap = new[] {
    new { Pattern = "SWGAS.COM"       , Name = "Southwest Gas" },
    new { Pattern = "georgiapower.com", Name = "Georgia Power" },
    // More specific first
    new { Pattern = "City of Austin"  , Name = "City of Austin" },   
    // Then more general
    new { Pattern = "Austin"          , Name = "Austin Electric Company" }   
    // And for everything else:
    new { Pattern = String.Empty      , Name = "Unknown" }
};

txtVar.Provider = providerMap.First(p => txtVar.BillText.IndexOf(p.Pattern) > -1).Name; 

更有可能,这些模式对的来源是可配置的,例如:

var providerMap =
    System.IO.File.ReadLines(@"C:\some\folder\providers.psv")
    .Select(line => line.Split('|'))
    .Select(parts => new { Pattern = parts[0], Name = parts[1] }).ToList();

@millimoose指出,匿名类型在方法间传递时不太有用。在这种情况下,我们可以定义一个简单的Provider类,并使用对象初始化器实现几乎相同的语法:

class Provider { 
    public string Pattern { get; set; } 
    public string Name { get; set; } 
}

var providerMap =
    System.IO.File.ReadLines(@"C:\some\folder\providers.psv")
    .Select(line => line.Split('|'))
    .Select(parts => new Provider() { Pattern = parts[0], Name = parts[1] }).ToList();

我不相信原帖作者已经确认(或否认)应该检查模式的顺序是否重要。 - millimoose
另一个好的方法,虽然初始化比 string[][2] 更多打字。但是,我承认这是一种更通用的解决方案,因为它允许键和值是不同类型的。 - Jim Mischel
@millimoose 匿名类型仍然很有用;请参见编辑 - Joshua Honig
1
如果最后一个提供程序的“Pattern”是空字符串,则可以确保捕获所有匹配,而无需担心“null”结果(其不会具有“Name”属性)。 - Joshua Honig
1
@millimoose 啊啊..我明白您的意思了(指出将匿名类型暴露给其他方法的困难)。在这种情况下,一个微不足道的 POCO 类也可以像这里使用的匿名类型一样。我只是想为教学目的展示一些“C#最佳实践”的示例。 - Joshua Honig
显示剩余5条评论

15

因为您似乎需要在返回值之前搜索键,所以使用Dictionary是正确的方法,但您需要遍历它。

// dictionary to hold mappings
Dictionary<string, string> mapping = new Dictionary<string, string>();
// add your mappings here
// loop over the keys
foreach (KeyValuePair<string, string> item in mapping)
{
    // return value if key found
    if(txtvar.BillText.IndexOf(item.Key) > -1) {
        return item.Value;
    }
}

编辑:如果您希望控制元素的评估顺序,请使用OrderedDictionary,并按照您想要它们被评估的顺序添加元素。


4
字典的缺点是你无法控制评估的顺序。可能会出现“SCE”转换为“南加州爱迪生公司”,而“SCEC”转换为“南卡罗来纳电力公司”的情况。在查找"SCE"之前,最好先查找"SCEC"。使用List<Pair>结构更好。您不需要字典哈希提供的快速查找。 - Mark Lakata
4
@MarkLakata OrderedDictionary这是一个特殊的数据结构,用于在字典中保留项目添加的顺序。它类似于普通的字典,但可以按照添加的顺序进行遍历。 - Asad Saeeduddin
没有任何迹象表明有这样的限制。但是OrderedDictionary也想到了,只是在这种情况下不太标准和似乎不必要。但是很好的建议,我会添加一个编辑。 - Serdalis
@MarkLakata 我非常非常怀疑在这种情况下顺序很重要。 - millimoose
@MarkLakata 我认为在这种情况下,最好向 OP 请求澄清,而不是过多地解读他们对控制结构的选择(老实说,400 行 else if 很少是一个有意的选择),并基于假设否定答案。 - millimoose
显示剩余6条评论

10

再来一个使用LINQ和Dictionary的例子

var mapping = new Dictionary<string, string>()
                        {
                            { "SWGAS.COM", "Southwest Gas" },
                            { "georgiapower.com", "Georgia Power" }
                            .
                            .
                        };

return mapping.Where(pair => txtvar.BillText.IndexOf(pair.Key) > -1)
              .Select(pair => pair.Value)
              .FirstOrDefault();
如果我们希望在没有键匹配时使用空字符串而不是null,可以使用 ?? 运算符:
return mapping.Where(pair => txtvar.BillText.IndexOf(pair.Key) > -1)
              .Select(pair => pair.Value)
              .FirstOrDefault() ?? "";
如果我们应该考虑字典包含类似的字符串,我们将添加一个排序方式,按字母顺序排序,最短的键将排在前面,这将在'SCEC'之前选择'SCE'。
return mapping.Where(pair => txtvar.BillText.IndexOf(pair.Key) > -1)
              .OrderBy(pair => pair.Key)
              .Select(pair => pair.Value)
              .FirstOrDefault() ?? "";

7
为了避免暴力遍历所有键的明显Schlemiel画家方法,让我们使用正则表达式!
// a dictionary that holds which bill text keyword maps to which provider
static Dictionary<string, string> BillTextToProvider = new Dictionary<string, string> {
    {"SWGAS.COM", "Southwest Gas"},
    {"georgiapower.com", "Georgia Power"}
    // ...
};

// a regex that will match any of the keys of this dictionary
// i.e. any of the bill text keywords
static Regex BillTextRegex = new Regex(
    string.Join("|", // to alternate between the keywords
                from key in BillTextToProvider.Keys // grab the keywords
                select Regex.Escape(key))); // escape any special characters in them

/// If any of the bill text keywords is found, return the corresponding provider.
/// Otherwise, return null.
string GetProvider(string billText) 
{
    var match = BillTextRegex.Match(billText);
    if (match.Success) 
        // the Value of the match will be the found substring
        return BillTextToProvider[match.Value];
    else return null;
}

// Your original code now reduces to:

var provider = GetProvider(txtvar.BillText);
// the if is be unnecessary if txtvar.Provider should be null in case it can't be 
// determined
if (provider != null) 
    txtvar.Provider = provider;

这个不区分大小写的问题对读者来说是一个微不足道的练习。
所有这些都没有强制规定要先查找哪些关键字 - 它将找到在字符串中最早的匹配项。 (然后是RE中首次出现的匹配项。)但是,您提到正在搜索大型文本; 如果.NET的RE实现很好,这应该比200个朴素的字符串搜索表现得更好。 (通过只对字符串进行一次遍历,并且可能通过合并编译后的RE中的公共前缀来实现。)
如果顺序对您很重要,则可能需要考虑寻找比.NET使用的更好的字符串搜索算法的实现。 (例如Boyer-Moore的变体。)

2
+1,也要教授历史 :) http://en.wikipedia.org/wiki/Schlemiel_the_Painter's_algorithm - Tommy Grovnes
@TommyGrovnes 现在我感觉老了,我从来没有把“Joel以前写博客”的时候看作是“历史”。 - millimoose
@millimoose 在这个行业里12年是很长的时间,但是时间是相对的 :) - Tommy Grovnes

4
你需要的是一个字典
Dictionary<string, string> mapping = new Dictionary<string, string>();
mapping["SWGAS.COM"] = "Southwest Gas";
mapping["foo"] = "bar";
... as many as you need, maybe read from a file ...

然后只需:
return mapping[inputString];

完成。

7
走在正确的道路上,但并不完全正确。输入字符串和密钥之间没有等价关系。请注意,原帖中使用了IndexOf。 - Steve
2
你没有解决如何从代码中删除if else结构的问题。你所做的只是让txtvar.Provider = "CPS Energy";更加简洁,即txtvar.Provider = mapping[searchString]; - Asad Saeeduddin

4

有一种方法可以实现这个(其他答案也提供了很多有效的选项):

void Main()
{
    string input = "georgiapower.com";
    string output = null;

    // an array of string arrays...an array of Tuples would also work, 
    // or a List<T> with any two-member type, etc.
    var search = new []{
        new []{ "SWGAS.COM", "Southwest Gas"},
        new []{ "georgiapower.com", "Georgia Power"},
        new []{ "City of Austin", "City of Austin"}
    };

    for( int i = 0; i < search.Length; i++ ){

        // more complex search logic could go here (e.g. a regex)
        if( input.IndexOf( search[i][0] ) > -1 ){
            output = search[i][1];
            break;
        }
    }

    // (optional) check that a valid result was found.
    if( output == null ){
        throw new InvalidOperationException( "A match was not found." );
    }

    // Assign the result, output it, etc.
    Console.WriteLine( output );
}

这个练习的主要教训是,创建一个巨大的switchif/else结构并不是最好的方法。

不区分大小写的匹配可以通过使用字典来实现。 - Asad Saeeduddin
@Asad - 谢谢。我从来不知道,但你是对的:https://dev59.com/sGw15IYBdhLWcg3wVaQa - Tim M.
1
你甚至可以在 IEqualityComparer 中实现模糊匹配,并将其插入字典中。 - Asad Saeeduddin
1
+1是因为它与原始的if/else语句具有相同的计算结果,但更易于阅读。使用字典方法无法达到相同的效果。 - Mark Lakata
3
尽管我同意传统使用字典的方式会比这种方法更加限制,但是我在答案中所使用的方法在功能上与Tim使用数组的方式完全相同。我们只是以不同的方式存储元素。从逻辑上讲,这些答案是等效的。 - Serdalis

1

有几种方法可以做到这一点,但为了简单起见,条件运算符可能是一个选择:

Func<String, bool> contains=x => {
    return txtvar.BillText.IndexOf(x)>-1;
};

txtvar.Provider=
    contains("SWGAS.COM")?"Southwest Gas":
    contains("georgiapower.com")?"Georgia Power":
    contains("City of Austin")?"City of Austin":
    // more statements go here 
    // if none of these matched, txtvar.Provider is assigned to itself
    txtvar.Provider;

请注意,结果是根据满足的更先前的条件而确定的。因此,如果txtvar.BillText="City of Austin georgiapower.com";,那么结果将是"Georgia Power"

0
你可以使用字典。
Dictionary<string, string> textValue = new Dictionary<string, string>();
foreach (KeyValuePair<string, string> textKey in textValue)
{
  if(txtvar.BillText.IndexOf(textKey.Key) > -1) 
   return textKey.Value;

}

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