返璞归真:If,For,Switch语句之后的生活,比如Data Structures Reminder

[原文发表地址] Back to Basics - Life After If, For and Switch - Like, a Data Structures Reminder

[原文发表时间] 2008-06-13 22:40

我在午餐时刚和我的一个好朋友一起进行了一对一编码学习。他正试着要让自己的编码技巧提升到另一个层次。就像我们通过锻炼可以提升一样,我觉得通过不断地编码和解决问题我们也可以提升。这也是为什么我提倡阅读尽可能多的代码以成为一个更优秀的开发者。同样这也是我开设“每周源代码”的原因之一。他说把这些写出来不要紧,因为也许其他人可以从中受益。代码和问题做了适当的更改以保护不那么单纯的人。

我们谈论的问题之一是有些编程人员/代码人/开发者的工具箱里只有一点点工具,比如if、for和switch。

我并不是在评判初级开发者和高级开发者。我在说的是成语和“词汇”。我觉得只会用if 、for和switch的电脑编程人员就等同于在每句句子里都用“好像”一样。 好像,你知道的,好像,他就是全部,好像,随便什么,我好像,亲爱的,而他,好像,唔,我就像我,你知道对吧?

说到英语,我怎么都不可能拥有像William F Buckley、Jr.-sized这样的词汇量,我也不相信要为了冗长而冗长。但是我坚信在对的场合选择正确的词是有百利而无一害的。我常常佩服那些能把一段冗长的段落精简为一句话却不失任何信息的人。

重构代码经常会给我一样光辉的感觉。这里有些实例,是我和我的朋友在午餐时间改的一些他程序中的代码,把冗长的段落变为简洁的句子。也许这不能算是一种罗列,不过倒是可以对我和其他人起到提示的作用,时刻考虑提前准备解决问题,就像,if,for,就像,switch,你懂?

大量的If有时就是Maps

他有一些代码,用来解析Web服务器转回的搞笑的XML文件。当然,XML的格式疯狂不是他的错。我们有时候不得不解析一些垃圾。他要确认一些特定值是否存在,然后要将其变为Enum。

  1: if (xmlNode.Attributes["someAttr"].Value.ToLower().IndexOf("fog") >= 0)
  2: {
  3: wt = MyEnum.Fog;
  4: }
  5: if (xmlNode.Attributes["someAttr"].Value.ToLower().IndexOf("haze") >= 0)
  6: {
  7: wt = MyEnum.Haze;
  8: }
  9: if (xmlNode.Attributes["someAttr"].Value.ToLower().IndexOf("dust") >= 0)
  10: {
  11: wt = MyEnum.Haze;
  12: }
  13: if (xmlNode.Attributes["someAttr"].Value.ToLower().IndexOf("rain") >= 0)
  14: {
  15: wt = MyEnum.Rainy;
  16: }
  17: 

然后这个就弄了40+的值。同时还有一些问题。

首先,他用IndexOf()和ToLower() to来表示“不考虑case,这个字符串包含这个其他字符串吗?”用ToLower()来做字符串比较经常是个很糟糕的想法,不只是因为Turkish i 问题(详见此处这里这里这里)。记得去看看对.NET中字符串的推荐

我们可以用一个辅助函数来使这些更简单易懂,这个辅助函数我们之后也会用到。它很好地诠释了我们到底想要干嘛。如果我们使用的是.NET3.5,那我们可以使用扩展函数,不过他用的是2.0版的。

  1: private static bool ContainsIgnoreCase(string s, string searchingFor)
  2: {
  3: return s.IndexOf(searchingFor, StringComparison.OrdinalIgnoreCase) >= 0;
  4: }
  5: 

第二,他不断地索引属性集合,也没有给其他的if设定"else 语句",所以每个属性索引和其他指令每次都不断地运行。他可以做一次索引,然后拉掉,然后检查他的值。

  1: string ws = xmlNode.Attributes["someAttr"].Value;
  2: if (ContainsIgnoreCase(ws, "cloud"))
  3: wt = MyEnum.Cloudy;
  4: else if (ContainsIgnoreCase(ws, "fog"))
  5: wt = MyEnum.Fog;
  6: else if (ContainsIgnoreCase(ws, "haze"))
  7: wt = MyEnum.Haze;
  8: else if (ContainsIgnoreCase(ws, "dust"))
  9: wt = MyEnum.Dust;
  10: else if (ContainsIgnoreCase(ws, "rain"))
  11: wt = MyEnum.Rainy;
  12: else if (ContainsIgnoreCase(ws, "shower"))
  13: 

再然后,作为提醒,这个继续了好多好多行。

我们是一步步讨论的,为了解释我的“点a到点d”,需要理解的时间。我想跳过b和c,以使渐进过程更明显,就好象在数学课上给你们看长除法的实例。

此时,我想指出的是他很显然在把字符串映射到enum。如果映射是直接的(这不是各种可怕的多到一的特定原因,他是一个“包含”问题而不是直接的等同比较)他可以解析字符串,像这样的最后一个参数不考虑情况的枚举:

  1: wt = (MyEnum)Enum.Parse(typeof(MyEnum), ws, true);

但是,他的映射有很多异常,XML也很混乱。简化一个步骤,我建议做一个映射。很多人一直用Hashtable,因为他们在.NET1.1中很多年了,但他们没有意识到Dictionary<TKey, TValue>有多可爱。

  1: var stringToMyEnum = new Dictionary<string, MyEnum>()
  2: {
  3: { "fog", MyEnum.Fog},
  4: { "haze", MyEnum.Fog},
  5: { "fred", MyEnum.Funky},
  6: //and on and on 
  7: };
  8: foreach (string key in stringToMyEnum.Keys)
  9: {
  10: if (ContainsIgnoreCase(ws, key))
  11: {
  12: wt = stringToMyEnum[key];
  13: break;
  14: }
  15: } 

他的输入数据很混乱,通过Key集来进行转换,每个key都要调用一下ContainsIgnoreCase,直到找到对的Enum。我建议他清理一下输入数据以免for loop,把整个运算变成一个简单的映射。这时,Dictionary就可以推入一些全局只读静态资源。

  1: string ws = xmlNode.Attributes["someAttr"].Value;
  2: //preproccessing over ws to tidy up
  3: var stringToMyEnum = new Dictionary<string, MyEnum>()
  4: {
  5: { "fog", MyEnum.Fog},
  6: { "haze", MyEnum.Fog},
  7: { "fred", MyEnum.Funky},
  8: //and on and on
  9: };
  10: wt = stringToMyEnum[key];

Switch什么时候激发

很多情况下,switch就这样发生了。我的意思是引入一个switch是为了处理一些不舒服的或者不自然的映射,结果它就失去控制了。它们告诉它们自己说它们会马上回来修好的,但到那个时候问题就已经一团乱了。

我的好兄弟设了一个函数,本来是打算清除他WinForms应用中的一个图标的,意图很简单,但是运行时却映射到了一个他无法控制的相当合理的Enum,然后就是一大堆命名了的图标。

这里的关键点在于他能控制图标但是不能轻易地控制enum(别人的代码等)。那样映射很不自然,对他的设计来说很假。

然后他知道的是没有赋予太多的考虑就被卷入一个switch语句。

  1: private void RemoveCurrentIcon()
  2: {
  3: switch (CurrentMyEnumIcon)
  4: {
  5: case MyEnum.Cloudy:
  6: CustomIcon1 twc = (CustomIcon1)FindControl("iconCloudy");
  7: if (twc != null)
  8: {
  9: twc.Visible = false;
  10: this.Controls.Remove(twc);
  11: }
  12: break;
  13: case MyEnum.Dust:
  14: CustomIcon2 twd = (CustomIcon2)FindControl("iconHaze");
  15: if (twd != null)
  16: {
  17: twd.Visible = false;
  18: this.Controls.Remove(twd);
  19: }
  20: break;
  21: //etc...for 30+ other case:

这里有些错误,除了switch本身就很搓。(我知道switch还是有用的,只是在这里没用)首先,在处理很多自定义类时,在这个例子中是CustomIcon1和CustomIcon2,看上去有共同的祖先,要记住这点。在这个例子里,他们共同的祖先就是Control。

接着,他可以控制他control的name。他的WinForms应用中的所有意图,Control的name都是任意。因为他的control都直接映射到他的Enum,那为什么不字面地直接映射呢?

  1: private void RemoveCurrentIcon(MyEnumIcon CurrentMyEnumIcon)
  2: {
  3: Control c = FindControl("icon" + CurrentMyEnumIcon);
  4: if(c != null)
  5: {
  6: c.Visible = false;
  7: Controls.Remove(c);
  8: }
  9: }

识别共享的基类和自然Enum到Control name的映射,把160行的switch变成了一个简单的辅助函数。

这很简单,而且很诱人。用if,for和switch跟电脑解释“按这样做”。不过,使用基本的声明性结构数据来描述数据类型可以帮你避免不必要的经典程序关键词,if, for和switch。

最终提醒:我们努力了一会儿,把8100行的长程序缩短至3950行。我想我们还可以再压掉1500到2000行。我们用新的Resharper4.0和CodeRush/Refactor!Pro的每日创建完成了重构。我会在接下来的几周对这两个工具做一些详细的介绍。