Global Travel-盛开的夕阳,走向家的方向

c/c++/c#/.net/ajax/英文技术文章/系统架构/项目管理

   :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

写给C++程序员的C# FAQ

作者 Andy McMullan  译者 熊节

感谢熊节先生和《程序员》杂志

这份FAQ将力图解答C++开发者第一次接触C#时可能遇到的基本问题。在阅读本FAQ之前,我推荐你先阅读《.NET框架FAQ》。

如果你对于本FAQ有任何意见、建议、批评或者指正,请给我e-mail:andy@andymcm.com。

请注意:本FAQ的内容仅仅是我对众多信息的阐述,这些信息的来源包括DOTNET邮件列表、微软公司的文档、以及使用C#语言的实际经验。我不保证本FAQ中解答的正确性,也不保证今后不修改其中的解答。

1. 简介

1.1 什么是C#?

C#是由微软(Microsoft)公司设计的一种程序设计语言。C#的语法大致是基于C/C++的,并且与Java有着惊人的相似性。微软对C#的描述如下:

C#是一种简单、现代化、面向对象并且类型安全的程序设计语言,它从C和C++衍生而来。C#(读作‘C sharp’)紧密地植根于C和C++的基础之上,因此C和C++程序员将可以立刻熟悉它。C#的设计意图是要将Visual Basic的高生产率和C++直接访问机器的强大能力结合起来。”

1.2 如何开发C#应用程序?

免费发布的.NET SDK中包括了C#命令行编译器(csc.exe)。另外,Visual Studio .NET也完全集成了对C#开发的支持。

1.3 在哪里可以下载.NET SDK和Visual Studio .NET?

你可以在http://msdn.microsoft.com/net下载.NET SDK。如果你订阅了MSDN宇宙版,那么你还可以下载Visual Studio .NET。

1.4  C#会取代Java吗?

C#与Java非常相似——在与C++的比较中,这两种语言表现出了类似的优势和不足。例如,这两种语言都有垃圾收集,但都没有模板。微软已经停止了Visual J++的生产,因此很难不把C#看成微软制造的Java替代品。

1.5  C#会取代C++吗?

答案很明显:不会。不过,在.NET环境中,C++也很难成为最佳的选择。为了让.NET运行时环境充分发挥它的威力,编程语言需要符合某些规则,其中一条重要的规则就是:语言的类型系统必须符合公共类型系统(CTS)的规定。不巧许多C++特性都不被CTS支持,例如多继承和模板等等,这使得C++注定不是.NET开发的首选语言。

对于这个问题,微软的回答是:他们提供了C++的受控扩展(ME),使程序员可以编写符合CTS要求的C++代码。他们提供了一些新的关键字,使程序员可以用CTS属性给C++类做上标记(例如用“__gc”标记表示垃圾收集)。但是,我也并不认为程序员会优先选择ME C++来进行新项目的开发,因为他们完全可以选择C#。从语法的角度来说,C#和C++非常相似;但是和C++不同,C#的设计思想就是要与.NET环境无缝结合,并充分利用.NET框架的功能。因此,ME C++存在的唯一理由就是:人们需要借助它来将现存的C++代码移植到.NET环境中来。

因此,作为对这个问题的回答,我猜测C++在.NET环境之外将继续作为一个重要的语言而存在,并将被用于将现存C++代码移植到.NET环境中。但是,当C++开发者要从头开发.NET应用程序时,我认为他们将选择C#。不过,时间将证明一切。

1.6 一个简单的C#程序像什么样子?

唉,我实在应该为我极度匮乏的想象力而向读者道歉。没错,你猜对了,下面就是著名的“Hello, World”……

class CApplication

{

          public static void Main() 

          {

                    System.Console.Write( "Hello, new .NET world." );

          }

}

(你不能把Main函数作为全局函数——C#中根本没有全局函数这种东西。)

1.7 C#是面向对象的吗?

是的,跟Java和C++一样,C#也是一种面向对象的语言。

1.8 C#有它自己的类库吗?

是,但也不完全是。和所有.NET语言(例如Visual Basic .NET和JScript .NET)一样,C#可以访问.NET类库。并没有一个仅属于C#的类库,.NET类库是所有语言共享的。

2. 基本类型

2.1 C#支持哪些标准类型?

C#支持的基本类型和C++非常相似,包括int、long、float、double、char、string、数组(array)、结构体(struct)和类(class)等等。但是,别想得太好,这些类型的名字虽然相似,但某些细节可能有所不同。例如,C#中的long型数据是64位,而C++中long型数据的大小取决于所处的平台(通常在32位平台上是32位,在64位平台上是64位)。另外,在C++中类和结构体几乎是完全相同的,但在C#中则不是这样。

2.2 C#中所有的类型都是从一个共同的基类派生来的吗?

既是也不是。所有的类型都会被作为object类(System.Object)的派生类来对待,但是如果要把值类型(例如int和float)的实例作为object派生类来处理,就必须通过所谓的“装箱(boxing)”过程将该实例转换成引用类型。从理论上来说,开发者可以完全忽略这个过程,让运行时来操心转换的细节。但是,这种隐式转换实际上会有一些副作用,可能会给粗心大意的开发者带来一些麻烦。

2.3 那么,我是否可以将值类型的实例传递给需要对象作为参数的方法呢?

是的,可以这样做。例如:

class CApplication

{

          public static void Main()

          { 

                    int x = 25; 

                    string s = "fred"; 

                

                    DisplayMe( x ); 

                    DisplayMe( s ); 

          } 

          

          static void DisplayMe( object o ) 

          { 

                    System.Console.WriteLine( "You are {0}", o ); 

          }

} 

这段程序的输出是:

You are 25

You are fred

2.4 值类型和引用类型之间有什么本质性的区别?

C#将类型分为两种:值类型和引用类型。绝大多数固有类型(例如int和char)都是值类型,结构体也是值类型。引用类型则包括类、接口(interface)、数组和字符串(string)。这里的根本思想很简单:值类型的实例表现的是实际数据,并被保存在栈上;而引用类型的实例表现的则是指向数据的指针或者引用,并被保存在堆上。

最令C++开发者感到迷惑的就是:对于哪些类型作为值来表现、哪些类型作为引用来表现,C#已经预先确定了。而C++开发者往往希望由自己来做这样的决定。

例如,在C++中,我们可以这样做:

int x1 = 3;               // x1 is a value on the stack

int *x2 = new int(3)    // x2 is a reference to a value on the heap

但是,在C#中,你就无法控制:

int x1 = 3;               // x1 is a value on the stack

int x2 = new int(); 

x2 = 3;             // x2 is also a value on the stack!

2.5 也就是说,int是值类型,object是引用类型。那么,int又怎么能从object派生出来呢?

其实,int并不是从object派生出来的。当一个int数被作为int数使用时,它就是一个值类型实例(保存在栈上)。但是,当它被作为object派生对象使用时,它就是一个引用类型实例,指向堆上的一个整数值。换句话说,当你把int值作为对象来对待时,运行时会自动地将int值转换成一个object引用。这个过程就被称为装箱(boxing)。这个转换的过程包括如下步骤:将int值的内容从栈拷贝到堆,然后创建一个object实例来指向它。解箱(unboxing)则是装箱的逆过程:将对象转换回栈上的值。

int x = 3;                // new int value 3 on the stack

object objx = x;          // new int on heap, set to value 3 - still have x=3 on stack

int y = (int)objx;          // new value 3 on stack, still got x=3 on stack and objx=3 on heap

2.6 C#用引用代替了指针。C#的引用和C++的引用是一样的吗?

并不完全一样。它们的基本思想是一样的,但是一个明显的差异是:C#的引用可以为null。因此,你不能期望C#引用始终指向合法的对象。如果你尝试使用一个为null的引用,系统就会抛出一个NullReferenceException异常。

例如,请看下列方法:

void displayStringLength( string s )

{

          Console.WriteLine( "String is length {0}", s.Length );

}               

这个方法的问题在于:如果你像下面这样调用它,它就可能抛出一个NullReferenceException异常。

string s = null;

displayStringLength( s );

当然,在某些情况下,你可能会认为NullReferenceException是一个完全可以接受的调用结果。但是,在这里,最好是将这个方法改写为下面这样:

void displayStringLength( string s )

{

          if( s == null )

                    Console.WriteLine( "String is null" );

          else

                    Console.WriteLine( "String is length {0}", s.Length );

}               

3. 类和结构体

3.1 在C++中,结构体基本上是多余的东西。为什么C#中还要有结构体呢?

C++中,结构体和类基本上是完全一样的,它们唯一的区别就在于默认的可见程度(结构体的默认可见程度是public,类的默认可见程度是private)。但是,在C#中,结构体和类有着非常大的差异。在C#中,结构体是值类型(保存在栈上),而类则是引用类型(保存在堆上)。另外,结构体不能继承结构体或者类,只能实现接口。结构体也没有析构子(destructor)。

3.2 C#支持多继承吗?

C#支持对接口的多继承,但是不支持对类的多继承。每个类最多只能继承一个类,但可以实现多个接口。

3.3 C#中的接口和C++中的抽象类是一样的吗?

不完全一样。C++中的抽象类不能实例化,但可以(并且经常的确)含有实现代码和/或数据成员;C#中的接口则不能含有任何实现代码或者数据成员,它只是一组方法名称和签名的集合。比起C++中的抽象类,C#中的接口跟COM中的接口更相似。

这两者之间另一个重要的区别是:C#的类只能继承一个类(不论是不是抽象的),但可以实现多个接口。

3.4 C#中的构造子和C++中的构造子是一样的吗?

非常相似。

3.5 C#中的析构子和C++中的析构子是一样的吗?

不!它们看上去差不多,但是实际上完全不同。首先,C#不保证在任何特定的时间点调用析构子,甚至不保证析构子会被调用。说实话,C#中的析构子其实就是Finalize方法,只不过换了个说法而已。或者说,它是一个会调用基类Finalize方法的Finalize方法。因此,下列代码:

class CTest

{

          ~CTest()

          {

                    System.Console.WriteLine( "Bye bye" );

          }

}

实际上就是:

class CTest

{

          protected override void Finalize() 

          {

                    System.Console.WriteLine( "Bye bye" ); 

                    base.Finalize();

          }

}

.NET框架Beta 2开始,程序员不能再像这样显式覆盖Finalize方法——你必须使用析构子语法。

3.6 既然C#中的析构子和C++中的析构子有这么大的差异,为什么微软还要使用一样的语法呢?

因为微软的人是坏蛋,他们想把你的脑子弄得一团糟。

3.7 什么是静态构造子?

所谓静态构造子(static constructor)就是为一个类(而不是类的实例)准备的构造子。当一个类被装载的时候,这个类的静态构造子就会被调用。

3.8 C#中所有的方法都是虚拟的吗?

不。和C++一样,方法默认是非虚拟(non-virtual)的,但你可以将它们标记为虚拟(virtual)方法。

3.9 在C#中如何声明纯虚函数?

abstract关键字来修饰一个方法,就可以将这个方法声明为纯虚的。自然,这个方法所属的类也必须同时被标记为抽象类。请注意,抽象方法不能有任何实现代码(这跟C++中的纯虚函数也有所不同)。

4. 异常

4.1 在C#中可以使用异常吗?

可以。实际上,在C#(以及整个.NET环境)中,异常是被推荐的错误处理机制。大多数.NET框架类都使用异常来表示错误。

4.2 哪些类型的对象可以作为异常抛出?

只有System.Exception类及其派生类的实例可以作为异常抛出。这和C++形成了鲜明的对比——在C++中,几乎任何类型的实例都可以作为异常抛出。

4.3 我可以定义自己的异常吗?

可以,只要让你的异常类继承System.Exception类就行了。更明确一点,微软推荐用户定义的异常继承System.ApplicationException类(这个类继承了System.Exception类)。

4.4 C#中有没有可以复用的标准异常?

有,其中的一些异常和标准的COM HRESULT值有一定的关系。下表展示了HRESULT和.NET(以及C#)异常的对应关系:

HRESULT

.NET异常

E_INVALIDARG

ArgumentOutOfRangeException,或者更普通的ArgumentException。如果提供的参数格式不对(例如URL以“htp://”开头,而不是以“http://”开头),可以选择使用FormatException

E_POINTER

ArgumentNullException

E_NOTIMPL

NotImplementedException

E_UNEXPECTED

通常认为InvalidOperationException是与之相对应的。InvalidOperationException通常被用于表示“对象无法执行所请求的操作”。

E_OUTOFMEMORY

OutOfMemoryException

其他你可能想要复用的标准异常还有IndexOutOfRangeException和ArithmeticException等等。

4.5 System.Exception类还有什么有用的特性吗?

是的,那就是StackTrace属性。这个属性提供了一个调用栈,其中记录了异常最初被抛出的位置。例如,下列代码:

using System;

 

class CApp 

{  

          public static void Main() 

          { 

                    try 

                    { 

                          f(); 

                    } 

                    catch( Exception e ) 

                    { 

                          Console.WriteLine( "System.Exception stack trace = \n{0}", e.StackTrace ); 

                    }

          } 

 

          static void f() 

          { 

                    throw new Exception( "f went pear-shaped" ); 

          } 

}

会产生如下输出:

System.Exception stack trace = 

          at CApp.f() 

          at CApp.Main()

但是,请注意:这种调用栈跟踪能力只有在调试版本中才会有。发布版本会把某些方法调用优化掉,这也就是说,调用栈将与你所想象的形式有所不同。

4.6 我应该在何时抛出异常?

这是一个颇有争议的话题,而且也部分取决于个人的喜好。不过,大多数人都接受这样一个观点:只有在出现“预料之外”的错误时,才应该抛出异常。如何判断一个错误是预料之中的还是预料之外的呢?各人的判断不同。这里有一个简单的例子:因为seek指针到达文件末尾而引起的文件读取错误应该属于预料之中的错误,而在堆上分配内存失败则应该属于预料之外的错误。

4.7 C#有throws子句吗?

没有。和Java不同,C#并不要求(甚至并不允许)开发者指定方法可以抛出的异常。

5. 运行时类型信息

5.1 如何在运行时检查对象的类型?

你可以使用is关键字。例如:

using System; 

 

class CApp

{

          public static void Main()

          { 

                    string s = "fred"; 

                    long i = 10; 

 

                    Console.WriteLine( "{0} is {1}an integer", s, (IsInteger(s) ? "" : "not ") ); 

                    Console.WriteLine( "{0} is {1}an integer", i, (IsInteger(i) ? "" : "not ") ); 

          }

          

          static bool IsInteger( object obj )

          { 

                    if( obj is int || obj is long )

                          return true; 

                    else 

                          return false;

          }

} 

会生成如下输出:

fred is not an integer 

10 is an integer

5.2 可以在运行时获取类型的名称吗?

可以。使用object类的GetType方法就可以获得该对象所属类型的名称。例如:

using System; 

 

class CTest

{

          class CApp 

          {

                    public static void Main()

                    { 

                          long i = 10; 

                          CTest ctest = new CTest(); 

 

                          DisplayTypeInfo( ctest ); 

                          DisplayTypeInfo( i ); 

                    }

                

                    static void DisplayTypeInfo( object obj ) 

                    { 

                          Console.WriteLine( "Type name = {0}, full type name = {1}", obj.GetType(), obj.GetType().FullName ); 

                    }

          }

}

会生成下列输出:

Type name = CTest, full type name = CTest 

Type name = Int64, full type name = System.Int64

6. 高级语言特性

6.1 什么是委托?

所谓委托(delegate),是从System.Delegate类派生而来的一个类。但是,C#语言对委托的声明提供了特殊的语法,使得它们看起来并不像是类。一个委托表现了一个特定签名[1]的方法,委托的一个实例则表现了特定对象(或类,如果方法是静态方法的话)中拥有特定签名的方法。例如:

using System;

delegate void Stereotype();

 

class CAmerican 

{

          public void BePatriotic() 

          { 

                    Console.WriteLine( "... <gulp> ... God bless America.");

          }

} 

 

class CBrit

{

          public void BeXenophobic()

          {

                    Console.WriteLine( "Bloody foreigners ... " );

          }

}

 

class CApplication

{ 

          public static void RevealYourStereotype( Stereotype[] stereotypes )

          { 

                    foreach( Stereotype s in stereotypes ) 

                          s();

          }

          

          public static void Main() 

          {

                    CAmerican chuck = new CAmerican(); 

                    CBrit edward = new CBrit(); 

 

                    // Create our list of sterotypes.

                    Stereotype[] stereotypes = new Stereotype[2];

                    stereotypes[0] = new Stereotype( chuck.BePatriotic );

                    stereotypes[1] = new Stereotype( edward.BeXenophobic );

                

                    // Reveal yourselves!

                    RevealYourStereotype(stereotypes ); 

          } 

}

会生成下列结果:

... <gulp>... God bless America.

Bloody foreigners ...

6.2 委托是否就和只有一个方法的接口一样?

从原理上来说,委托可以和“单方法接口”一样地使用。这两者之间最主要的区别就在于:对于接口来说,方法的名称是固定的;而对于委托来说,就只有签名是固定的,而方法名称则可以变化,正如上面的范例所显示的那样。

6.3 C#中与QueryInterface函数等价的是什么?

as关键字。例如:

using System; 

 

interface IPerson

{ 

          string GetName(); 

}

 

interface IPerson2 : IPerson 

{ 

          int GetAge(); 

} 

 

class CPerson : IPerson 

{ 

          public CPerson( string name )

          {

                    m_name = name;

          } 

          

          // IPerson 

          public string GetName()

          {

                    return m_name;

          }

           

          private string m_name; 

}

 

class CPerson2 : IPerson2

{ 

          public CPerson2( string name, int age ) 

          { 

                    m_name = name; 

                    m_age = age; 

          }

          

          // IPerson2 

          public string GetName() { return m_name; } 

          public int GetAge() { return m_age; } 

          private string m_name; private int m_age;

} 

 

public class CApp 

{

          public static void Main()

          { 

                    CPerson bob = new CPerson( "Bob" ); 

                    CPerson2 sheila = new CPerson2( "Sheila", 24 ); 

                

                    DisplayAge( bob );

                    DisplayAge( sheila ); 

          }

          

          static void DisplayAge( IPerson person )

          {

                    IPerson2 person2 = person as IPerson2;   // QueryInterface lives on !!! 

                    if( person2 != null )

                          Console.WriteLine( "{0} is {1} years old.", person2.GetName(), person2.GetAge() ); 

                    else 

                          Console.WriteLine( "Sorry, don't know {0}'s age.", person.GetName() ); 

          } 

}

上述程序会生成下列输出:

Sorry, don't know Bob's age.

Sheila is 24 years old.

7. 和C++的主要差异

7.1 我新建了一个对象,但是要怎么销毁它呢?

你无法主动销毁对象。你不能显式调用析构子,也没有delete运算符可供使用。不必担心,垃圾收集器总会销毁你的对象的。

7.2 我试图在栈上创建一个对象,但C#编译器就不干了。应该怎么办?

C++不同,你不能在栈上创建类的实例。类的实例总是生存在堆上的,并由垃圾收集器管理它们的生存周期。

7.3 我定义了一个析构子,但它没有被调用到。为什么?

C#中的析构子实际上只是Finalize方法的一个实现,运行时也不保证一定调用Finalize方法。C#中的析构子语意和C++的析构子语意是完全不同的。

7.4 大部分C#基本类型都有着和C++基本类型相同的名称,它们是相同的东西吗?

不是。C#中的char实际上跟C++中的wchar_t是一样的。C#中所有的字符(以及字符串)都是Unicode的。C#中的整数都有确定的大小,而C++中的整数大小则取决于运行的平台。例如,C#中的int总是32位;而C++中的int通常在32位处理器平台上是32位,在64位处理器平台上则是64位。

8. 杂项

8.1 使用“==”进行的字符串比较似乎是大小写敏感的。怎样才能进行大小写不敏感的字符串比较呢?

使用String.Compare方法。这个方法的第三个参数是一个布尔值,用于指定是否忽略大小写。

"fred" == "Fred"          // false

System.String.Compare( "fred", "Fred", true )          // true

8.2 我发现有些字符串使用了“@”标记,而有些则没有。这个标记是什么意思?

The @ symbol before a string literal means that escape sequences are ignored. This is particularly useful for file names, e.g.

如果在字符串前面加上“@”标记,就意味着这个字符串中的转义序列将被忽略。在表示文件名时,这个标记特别有用。例如:

string fileName = "c:\\temp\\test.txt"

就等价于:

string fileName = @"c:\temp\test.txt"

8.3 C#支持可变参数吗?

可以。使用params关键字,你可以将参数指定为特定类型(例如int)的序列;如果需要极大的灵活性,甚至还可以将参数类型指定为object。使用这种办法的典型就是System.Console.WriteLine方法。

8.4 如何处理命令行参数?

就像这样:

using System;

 

class CApp

{

          public static void Main( string[] args )

          {

                    Console.WriteLine( "You passed the following arguments:" );

                    foreach( string arg in args )

                          Console.WriteLine( arg );

          }

}

8.5 C#会对数组边界进行检查吗?

是的。如果数组越界,系统会抛出一个IndexOutOfRange异常。

8.6 如何保证我的C#类可以和其他.NET语言互操作?

只要保证C#代码符合公共语言子集(CLS)即可。为了保证这一点,请将[assembly:CLSCompliant(true)]这个全局属性加入到你的C#源文件中。这样,如果你使用了不符合CLS的C#特性,编译器就会报错。

9. 资源

9.1 推荐书目

在下面的推荐书目中,既有我个人喜欢的,也有其他C#开发者推荐的。

9.2 网络资源

9.3 范例代码和工具

[1] 译注:此处的“签名(signature)”是指“方法的参数列表和返回值”,不包括方法名在内。

posted on 2006-10-24 23:07  Seraph's Zone(WelCome)  阅读(580)  评论(0编辑  收藏  举报