Top Ten Traps in C# for C++ Programmers中文版(下篇)
作者:Jesse Liberty
译者:荣耀
【译序:C#入门文章。译文中所有程序调试环境均为Microsoft Visual Studio.NET 7.0 Beta2和 Microsoft .NET Framework SDK Beta2。】
陷阱六.虚方法必须被显式重载
在C#中,如果程序员决定重载一个虚方法,他(她)必须显式使用override关键字。
让我们考察一下这样做的好处。假定公司A写了一个Window类,公司B购买了公司A的Window类的一个拷贝作为基类。公司B的程序员从中派生【译注:原文为...using...,从下文来看,显然是“派生”之意。事实上,使用类的方式还有“组合”(也有说为“嵌入”或“包容”(COM语义)等等),后者不存在下文所描述的问题】出ListBox类和RadioButton类。公司B的程序员不知道或不能控制Window类的设计,包括公司A将来对Window类可能做的修改。
现在假定公司B的程序员决定为ListBox类加入一个Sort方法:
public class ListBox : Window
{
public virtual void Sort() {}
}
这是没有问题的—直到公司A的Window类作者发布了Window类的版本2,公司A的程序员向Window类也加入了一个public的Sort方法:
public class Window
{
public virtual void Sort() {}
}
在C++中,Window类新的虚方法Sort将会作为ListBox虚方法的基类方法。当你试图调用Window的Sort时,实际上调用的是ListBox的Sort。C#中虚方法【译注:原文写成virtual function】永远被认为是虚拟调度的根。这就是说,只要C#找到了一个虚方法,它就不会再沿着继承层次进一步寻找了,如果一个新的Sort虚方法被引入Window,ListBox的运行时行为不会被改变。当ListBox再次被编译时,编译器会发出如下警告:
"\class1.cs(54,24): warning CS0114: 'ListBox.Sort()' hides inherited member 'Window.Sort()'.
如果要使当前成员重载实现,可加入override关键字。否则,加上new关键字。
如果想要移去这个警告,程序员必须明确指明他的意图。可以将ListBox的Sort方法标为new,以指明它不是对Window的虚方法的重载:
public class ListBox : Window
{
public new virtual void Sort() {}
}
这样编译器就不会再警告。另一方面,如果程序员想重载Window的方法,只要显式加上override关键字即可。
陷阱七:不可以在头部进行初始化
C#里的初始化不同于C++。假定你有一个类Person,它有一个私有成员变量age;一个派生类Employee,它有一个私有成员变量salaryLeverl。在C++中,你可以在Employee构造器的成员初始化列表部分初始化salaryLevel:
Employee::Employee(int theAge, int theSalaryLevel):
Person(theAge) // 初始化基类
salaryLevel(theSalaryLevel) // 初始化成员变量
{
// 构造器体
}
在C#中,这个构造器是非法的。尽管你仍可以如此初始化基类,但对成员变量的初始化将导致一个编译时错误。你可以在成员变量声明处对其赋初始值:
Class Employee : public Person
{
// 在这儿声明
private salaryLevel = 3; //初始化
}
【译注:以上代码有误LC#中,正确写法如下:
class Employee: Person
{
private int salaryLevel = 3;
}
】
你不需要在每一个类声明的后面都加上一个分号。每一个成员都必须要有显式的访问级别声明。
陷阱8.不能把布尔值转换为整型值
在C#中,布尔值(true、false)不同于整型值。因此,不能这么写:
if ( someFuncWhichReturnsAValue() )//【译注:假定这个方法不返回布尔值】
也不能指望如果someFuncWhichReturnsAValue返回一个0它将等于false,否则为true。一个好消息是误用赋值操作符而不是相等操作符的老毛病不会再犯了。因此,如果这么写:
if ( x = 5 )
将会得到一个编译时错误,因为x = 5的结果为5,而它不是布尔值。
【译注:以下是C++里一不小心会犯的逻辑错误,编译器不会有任何提示L运行得很顺畅,不过结果并不是你想要的:
C++:
#include "stdafx.h"
int main(int argc, char* argv[])
{
int n = 0;
if (n = 1)//编译器啥都没说L一般推荐写为1 == n,万一写成1 = n编译器都不同意J
{
printf("1\n");
}
else
{
printf("0\n");
}
return 0;
}
以上运行结果为1,这未必是你想要的。
C#:
using System;
public class RyTestBoolApp
{
public static void Main()
{
int n = 0;
if (n = 1)//编译器不同意J无法将int转换成bool
{
Console.WriteLine("1");
}
else
{
Console.WriteLine("0");
}
}
}
但如果是这种情况:
bool b = false;
if (b = true)
...
不管是C++还是C#都没招L
】
【译注:C++程序员一般是喜欢这种自由的写法:
if (MyRef)
if (MyInt)
但在C#里,必须写成:
if (MyRef != null)//或if (null != MyRef)
if (MyInt != 0)//或if (0 != MyInt)
等。
】
陷阱九.switch语句不可“贯穿”【译注:即fall through,Beta2的联机文档就是如此译法】
在C#中,如果在case语句里有代码的话,那它就不可“贯穿”到下一句。因此,尽管下面代码在C++里合法,但在C#中则不然:
switch (i)
{
case 4:
CallFuncOne();
case 5: // 错误,不可以“贯穿”
CallSomeFunc();
}
为了达到这个目的,需要显式地使用goto语句:
switch (i)
{
case 4:
CallFuncOne();
goto case 5;
case 5:
CallSomeFunc();
}
如果case语句没做任何事(里面没有代码)就可以“贯穿”:
switch (i)
{
case 4: // 可以“贯穿”
case 5: // 可以“贯穿”
case 6:
CallSomeFunc();
}
【译注:以下是使用switch的完整例子,它还说明了switch语句的参数类型可以是字符串,此例同时还演示了属性的使用方法。
using System;
class RySwitchTest
{
public RySwitchTest(string AStr)
{
this.StrProperty = AStr;
}
protected string StrField;
public string StrProperty
{
get
{
return this.StrField;
}
set
{
this.StrField = value;
}
}
public void SwitchStrProperty()
{
switch (this.StrProperty)
{
case ("ry01"):
Console.WriteLine("ry01");
break;
case ("ry02"):
Console.WriteLine("ry02");
break;//如果这一行注释掉,编译器会报控制不能从一个case标签(case "ry02":)贯穿到另一个标签,如果你确实需要,可以这么写:goto case ("ry03");或goto default。
case ("ry03"):
Console.WriteLine("ry03");
break;
default:
Console.WriteLine("default");
break;
}
}
}
class RySwitchTestApp
{
public static void Main()
{
RySwitchTest rst = new RySwitchTest("ry02");
rst.SwitchStrProperty();
}
}
】
陷阱十.C#需要明确的赋值操作
C#要求必须明确地进行赋值操作,这就意味所有变量在使用前必须被赋值。因此,尽管你可以声明未初始化的变量,但在它拥有值之前是不可以被传递到方法的。
这就引出了一个问题—当你仅仅是想将变量用作一个“出”参数按引用传递给方法时。例如,假定有个方法,返回当前的小时、分钟和秒。如果这么写:
int theHour;
int theMinute;
int theSecond;
timeObject.GetTime( ref theHour, ref theMinute, ref theSecond)
编译将出错,因为在使用theHour、theMinute和theSecond前,它们没有被初始化:
Use of unassigned local variable 'theHour'
Use of unassigned local variable 'theMinute'
Use of unassigned local variable 'theSecond'
可以将它们初始化为0或者其它什么无伤大雅的值以让讨厌的编译器安静下来:
int theHour = 0;
int theMinute = 0;
int theSecond = 0;
timeObject.GetTime( ref theHour, ref theMinute, ref theSecond)
但是这种写法实在太愚蠢!我们的本意不过是想把这些变量按引用传递到GetTime,在其中改变它们的值。为了解决这个问题,C#提供了out参数修饰符。这个修饰符避免了对引用参数也要初始化的需求。例如,为GetTime提供的参数没有提供给方法任何信息,它们仅仅是想从方法里取得信息。因此,把这三个参数都标记为out型的,就避免了在方法外初始化它们的需要。但当从被传入的方法返回时,out参数必须被赋值。下面是改变后的GetTime参数声明:
public void GetTime(out int h, out int m, out int s)
{
h = Hour;
m = Minute;
s = Second;
}
下面则是对GetTime方法的新的调用方式:
timeObject.GetTime( out theHour, out theMinute, out theSecond);
【译注:完整示例如下:
C#:[例1:使用ref修饰的方法参数]
using System;
class RyRefTest
{
public RyRefTest()
{
this.IntField = 1;
this.StrField = "StrField";
}
protected int IntField;
protected string StrField;
public void GetFields(ref int AInt, ref string AStr)
{
AInt = this.IntField;
AStr = this.StrField;
}
}
class RyRefTestApp
{
public static void Main()
{
RyRefTest rrt = new RyRefTest();
int IntVar = 0;//如果是int IntVar; 编译器会报使用了未赋值的变量IntVar
string StrVar = "0";//如果是string StrVar; 编译器会报使用了未赋值的变量StrVar
rrt.GetFields(ref IntVar, ref StrVar);
Console.WriteLine("IntVar = {0}, StrVar = {1}", IntVar, StrVar);
}
}
C#:[例2:使用out修饰的方法参数]
using System;
class RyRefTest
{
public RyRefTest()
{
this.IntField = 1;
this.StrField = "StrField";
}
protected int IntField;
protected string StrField;
public void GetFields(out int AInt, out string AStr)
{
AInt = this.IntField;
AStr = this.StrField;
}
}
class RyRefTestApp
{
public static void Main()
{
RyRefTest rrt = new RyRefTest();
int IntVar;//这样就可以了,如果写成int IntVar = 0;当然也没问题J
string StrVar; //这样就可以了,如果写成string StrVar = "0";当然也没问题J
rrt.GetFields(out IntVar, out StrVar);
Console.WriteLine("IntVar = {0}, StrVar = {1}", IntVar, StrVar);
}
}
】
【译注:如欲了解更多,请参阅A Comparative Overview of C#中文版(上篇)、A Comparative Overview of C#中文版(下篇)、C#首席设计师Anders Hejlsberg专访。】
-全文完-