Visual Studio:针对 Java 开发人员的 C# 编程语言(转自MSDN)
• | Java 和 C# 都源于 C++,并且共有 C++ 的一些特征。 |
• | 两种语言都需要编译成中间代码,而不是直接编译成纯机器码。Java 编译成 Java 虚拟机 (Java Virtual Machine, JVM) 字节码,而 C# 则编译成公共中间语言 (Common Intermediate Language, CIL)。 |
• | Java 字节码是通过称为 Java 虚拟机 (JVM) 的应用程序执行的。类似地,已编译的 C# 程序由公共语言运行库 (Common Language Runtime, CLR) 执行。 |
• | 除了一些细微的差别以外,C# 中的异常处理与 Java 非常相似。C# 用 try...catch 构造来处理运行时错误(也称为异常),这和 Java 中是完全一样的。System.Exception 类是所有 C# 异常类的基类。 |
• | 同 Java 一样,C# 是强类型检查编程语言。编译器能够检测在运行时可能会出现问题的类型错误。 |
• | 同 Java 一样,C# 提供自动垃圾回收功能,从而使编程人员避免了跟踪分配的资源。 |
• | Java 和 C# 都支持单一继承和多接口实现。 |
现在让我们看一看本文涵盖的重要差异:
• | C# 语言引入了一些新的语言构造,如 foreach、索引器、属性、委托、运算符重载和其他语言构造。在本文后面我们将更详细地讨论这些构造。 |
我们需要知道,两种语言在源程序的文件命名约定和结构上有一些不同:
包含 C# 类的文件的命名约定与 Java 有点不同。首先,在 Java 中,所有源文件的扩展名都为 .java。每个源文件都包含一个顶层公共类声明,并且类名必须与文件名相匹配。换句话说,一个用公共范围声明的名为 Customer 的类必须定义在具有名称 Customer.java 的源文件中。
而 C# 源代码是由 .cs 扩展名表示的。与 Java 不同,源文件可以包含多个顶层公共类声明,而文件名不需要与任何类名相匹配。
在 Java 和 C# 中,源代码以按一定顺序排列的顶层声明开始。Java 和 C# 程序中的声明只存在少许差别。
在 Java 中,我们可以用 package 关键字将类组合在一起。打包的类必须在源文件的第一个可执行的行中使用 package 关键字。接着出现的是需要访问其他包中的类的任何导入语句,之后是类声明,比如:
package ; import .; class Customer { ... }
C# 使用命名空间的概念,通过 namespace 关键字将逻辑上相关的类组合在一起。这些做法类似于 Java 包,而具有相同名称的类可以出现在两个不同的命名空间中。要访问定义在当前命名空间之外的命名空间中的类,我们可以使用紧跟该命名空间名的 using 关键字,如下所示:
using .; namespace { class Customer { ... } }
注意,using 语句可以完全合法地放在命名空间声明中,在这种情况下,这样导入的命名空间就形成了包含命名空间的一部分。
Java 不允许在相同的源文件中有多个包,而 C# 允许在一个 .cs 文件中有多个命名空间:
namespace AcmeAccounting { public class GetDetails { ... } } namespace AcmeFinance { public class ShowDetails { ... } }
同 Java 一样,通过提供类的完全限定名(如 System.Data.DataSet 或上面的示例中的 AcmeAccounting.GetDetails),我们可以在没有命名空间的 using 引用的情况下访问 .NET 或用户定义的命名空间中的类。
完全限定名可能会变得很长而不便于使用,在这种情况下,我们可以使用 using 关键字来指定一个简称或别名,以提高代码的可读性。
在下面的代码中,创建了一个别名来引用由一个虚构的公司所编写的代码:
using DataTier = Acme.SQLCode.Client; using System; public class OutputSales { public static void Main() { int sales = DataTier.GetSales("January"); Console.WriteLine("January's Sales: {0}", sales); } }
注意 WriteLine() 的语法,格式字符串中带有 {x},其中 x 表示在此处要插入的值的参数列表的位置。假定 GetSales() 方法返回 500,则该应用程序的输出将为:
January's Sales: 500
与 C 和 C++ 相似,C# 包括预处理器指令,预处理器指令提供了有条件地跳过源文件的某些部分、报告错误和警告条件,以及描述源代码的不同部分的能力。使用“预处理指令”这个术语只是为了与 C 和 C++ 编程语言保持一致,因为 C# 并不包括单独的预处理步骤。有关 C# 预处理器指令的完整列表,请参见 C# 预处理器指令。
在这一部分中,我们讨论这两种语言之间的相似点和不同点。一些主要的不同点有:
• | 常量声明— Java 为此而使用 final 关键字,而 C# 使用关键字 const 或 readonly。 |
• | 复合数据类型— 在 Java 中,我们可以使用类关键字来创建作为没有方法的类的复合数据类型,但是 C# 为此提供了 struct,同 C 中一样。 |
• | 析构函数— C# 允许我们创建在销毁类的实例之前调用的析构函数方法。在 Java 中,可以提供 finalize() 方法来包含在将对象作为垃圾回收之前清除资源的代码。在 C# 中,由类析构函数来提供此功能。析构函数类似一个没有参数并前面带有波形符“~”的构造函数。 |
• | 函数指针 — C# 提供一个称为 delegate 的构造来创建类型安全的函数指针。Java 没有任何与之对等的机制。 |
C# 提供了在 Java 中可用的所有数据类型,并且增加了对无符号数和新的 128 位高精度浮点类型的支持。
在 Java 中,对于每个基本数据类型,核心类库都提供了一个包装类来将其表示为 Java 对象。例如,Integer 类包装 int 数据类型,而 Double 类包装 double 数据类型。
而在 C# 中,所有的基本数据类型都是 System 命名空间中的对象。对于每个数据类型,都提供一个简称或别名。例如,int 是 System.Int32 的简称,而 double 是 System.Double 的简写形式。
下面的列表给出了 C# 数据类型及其别名。可以看到,前 8 个对应于 Java 中可用的基本类型。不过,请注意,Java 的 boolean 在 C# 中称为 bool。
C# 数据类型 | ||||
简称 | .NET类 | 类型 | 宽度 | 范围(位) |
byte |
System.Byte |
无符号整数 |
8 |
-128 到 127 |
sbyte |
System.SByte |
有符号整数 |
8 |
-128 到 127 |
int |
System.Int32 |
有符号整数 |
32 |
-2,147,483,648 到 2,147,483,647 |
uint |
System.UInt32 |
无符号整数 |
32 |
0 到 4294967295 |
short |
System.Int16 |
有符号整数 |
16 |
-32,768 到 32,767 |
ushort |
System.UInt16 |
无符号整数 |
16 |
0 到 65535 |
long |
System.Int64 |
有符号整数 |
64 |
-922337203685477508 到 922337203685477507 |
ulong |
System.UInt64 |
无符号整数 |
64 |
0 到 18446744073709551615 |
float |
System.Single |
单精度浮点类型 |
32 |
-3.402823e38 到 3.402823e38 |
double |
System.Double |
双精度浮点类型 |
64 |
-1.79769313486232e308 到 1.79769313486232e308 |
char |
System.Char |
单个 Unicode 字符 |
16 |
用在文本中的 Unicode 符号 |
bool |
System.Boolean |
逻辑 Boolean 类型 |
8 |
true 或 false |
object |
System.Object |
所有其他类型的基本类型 |
|
|
string |
System.String |
字符序列 |
|
|
decimal |
System.Decimal |
可以表示具有 29 个有效位的小数的精确分数或整数类型 |
128 |
-2 x 10-96 到 2 x 1096 |
因为 C# 将所有的基本数据类型都表示为对象,所以按照基本数据类型来调用对象方法是可能的。例如:
int i=10; Console.WriteLine(i.ToString());
借助于自动装箱和拆箱,可以达到此目的。更多信息请参见装箱和拆箱。
与 C/C++ 相似,在 C# 中可以使用枚举来组合已命名常量,而在 Java 中不能使用枚举。下面的示例定义了一个简单的 Color 枚举。
public enum Color {Green, Orange, Red, Blue}
还可以为枚举赋整数值,如下面的枚举声明所示:
public enum Color {Green=10, Orange=20, Red=30, Blue=40}
下面的程序调用 Enum 类型的 GetNames 方法来显示枚举的可用常量。然后,它将值赋给枚举,并显示该值。
using System; public class TypeTest { public static void Main() { Console.WriteLine("Possible color choices: "); //Enum.GetNames returns a string array of named constants for the enum foreach(string s in Enum.GetNames(typeof(Color))) { Console.WriteLine(s); } Color FavoriteColor = Color.Blue; Console.WriteLine("Favorite Color is {0}",FavoriteColor); Console.WriteLine("Favorite Color value is {0}", (int)FavoriteColor); } }
在运行之后,该程序将显示如下结果:
Possible color choices: Green Orange Red Blue Favorite Color is Blue Favorite Color value is 40
在 Java 和 C# 中,字符串类型表现出相似的行为,只有一些细微的差别。二者的字符串类型均是不可改变的,这意味着一旦字符串创建完毕,就不能改变字符串的值。在二者的实例中,看起来像修改字符串实际内容的方法实际上创建一个新的字符串供返回,而保留原始的字符串不变。在 C# 和 Java 中,比较字符串值的过程是不同的。在 Java 中,为了比较字符串的值,开发人员需要按照字符串类型调用 equals() 方法,正如在默认情况下 == 运算符比较引用类型一样。在 C# 中,开发人员可以使用 == 或 != 运算符来直接比较字符串的值。在 C# 中,尽管字符串是引用类型,但是在默认情况下,== 和 != 运算符将比较字符串的值而不是引用。在本文后面,我们将讨论值类型和引用。
正如在 Java 中一样,C# 开发人员不应该使用字符串类型来连接字符串,以避免每次连接字符串时都创建新的字符串类的开销。相反,开发人员可以使用 System.Text 命名空间中的 StringBuilder 类,它在功能上等同于 Java 中的 StringBuffer 类。
C# 提供了避免在字符串常量中使用转义序列(如代表制表符的“\t”或代表反斜杠字符的“\”)的功能。要这样做,可以在为字符串赋值之前使用 @ 符号来声明字符串。下面的示例显示了如何使用转义字符以及如何为字符串赋值:
//Using escaped characters string path = "\\\\FileShare\\Directory\\file.txt"; //Using String Literals string escapedPath = @"\\FileShare\Directory\file.txt";
Java 和 C# 遵守相似的数据类型自动转换和强制转换规则。
同 Java 一样,C# 既支持隐式类型转换又支持显式类型转换。在扩大转换的情况下,转换是隐式的。例如,下面从 int 到 long 的转换是隐式的,如同 Java 中的一样:
int intVariable = 5; long l = intVariable;
下面是 .NET 数据类型之间的隐式转换列表:
隐式转换 | |
源类型 | 目标类型 |
byte |
short, ushort, int, uint, long, ulong, float, double 或 decimal |
sbyte |
short, int, long, float, double, ? decimal |
int |
long, float, double, 或 decimal |
uint |
long, ulong, float, double, 或 decimal |
short |
int, long, float, double, 或 decimal |
ushort |
int, uint, long, ulong, float, double, 或 decimal |
long |
float, double, 或 decimal |
ulong |
float, double, 或 decimal |
float |
double |
char |
ushort, int, uint, long, ulong, float, double, 或 decimal |
可以使用与 Java 一样的语法对希望显式转换的表达式进行强制转换:
long longVariable = 5483; int intVariable = (int)longVariable;
显式转换 | |
源类型 | 目标类型 |
byte |
sbyte 或 char |
sbyte |
byte, ushort, uint, ulong, 或 char |
int |
sbyte, byte, short, ushort, uint, ulong, 或 char |
uint |
sbyte, byte, short, ushort, int, 或 char |
short |
sbyte, byte, ushort, uint, ulong, 或 char |
ushort |
sbyte, byte, short, 或 char |
long |
sbyte, byte, short, ushort, int, uint, ulong, 或 char |
ulong |
sbyte, byte, short, ushort, int, uint, long, 或 char |
float |
sbyte, byte, short, ushort, int, uint, long, ulong, char, 或 decimal |
double |
sbyte, byte, short, ushort, int, uint, long, ulong, char, float, 或 decimal |
char |
sbyte, byte, 或 short |
decimal |
sbyte, byte, short, ushort, int, uint, long, ulong, char, float, 或 double |
C# 支持两种变量类型:
• | 值类型 — 这些是内置的基本数据类型,例如 char、int、float 和用 struct 声明的用户定义类型。 |
• | 引用类型 — 从基本类型构造而来的类和其他复杂数据类型。这种类型的变量并不包含类型的实例,而只是包含对实例的引用。 |
让我们略微深入地研究一下这个问题。如果我们创建两个值类型变量 i 和 j,比如:
int i = 10; int j = 20;
则 i 和 j 彼此完全独立,并且分配了不同的内存位置:
如果我们改变这些变量中的某一个的值,另一个自然不会受到影响。例如,如果我们有一个这样的表达式:
int k = i;
则变量之间仍然没有联系。也就是说,之后如果我们改变 i 的值,k 还会保留赋值时 i 具有的值。
然而,引用类型的做法却不同。例如,我们可以这样声明两个变量:
myClass a = new myClass(); myClass b = a;
现在,因为类是 C# 中的引用类型,所以 a 称为对 myClass 的引用。上面两行中的第一行在内存中创建了 myClass 的一个实例,并且将 a 设置为引用该实例。因此,当我们将 b 设置为等于 a 时,它就包含了对内存中类的引用的重复。如果我们现在改变 b 中的属性,a 中的属性就将反映这些改变,因为这两者都指向内存中的相同对象,如下图所示:
这种将值类型转换为引用类型的过程称为装箱。而相反的将引用类型转换为值类型的过程就称为拆箱。如下面的代码所示:
int valueVariable = 10; // boxing object obj = refVariable; // unboxing int valueVariable = (int) refVariable;
Java 需要我们手动执行这样的转换。通过构造这样的对象,可以将基本数据类型转换成包装类的对象(装箱)。同样,通过调用这种对象中的适当方法,也可以从包装类的对象中提取基本数据类型的值(拆箱)。有关装箱的更多信息,请参见 装箱转换;有关拆箱的更多信息,请参见 拆箱转换。
C# 提供了 Java 支持的所有可用运算符,如下表所示。在表的末尾,您将看到一些新的运算符,它们可以在 C# 中使用而不可以在 Java 中使用:
运算符 | |
类别 | 符号 |
[Text] |
[Text] |
一元 |
++ -- + - ! ~ () |
乘法 |
* / % |
加法 |
+ - |
移位 |
<< >> |
关系 |
< > <= >= instanceof |
相等 |
== != |
逻辑与 |
& |
逻辑异或 |
^ |
逻辑或 |
| |
条件与 |
&& |
条件或 |
|| |
条件 |
? : |
赋值 |
= *= /= %= += -= <<= >>= &= ^= |= |
操作数的类型 |
typeof |
操作数的大小 |
sizeof |
执行溢出检查 |
checked |
取消溢出检查 |
unchecked |
唯一不可以在 C# 中使用的 Java 运算符是 >>> 移位运算符。之所以在 Java 中存在此运算符,是因为该语言中缺乏无符号变量,例如在需要右移位以在最高有效比特位插入 1 时。
然而,C# 支持无符号变量,因而 C# 只需要标准 >> 运算符。取决于操作数是否带有符号,此运算符产生不同的结果。右移一个无符号数会在最高有效比特位插入 0,而右移一个有符号数则会复制前面的最高有效比特位。
如果对于分配给正在使用的数据类型的比特数来说结果太大,则算术运算会产生溢出。对于特定的整数算术运算,通过使用 checked 和 unchecked 关键字,可以检查或忽略这样的溢出。如果表达式是一个使用 checked 的常量表达式,则会在编译时产生错误。
下面这个简单的示例说明了这两个运算符的用法
using System; public class Class1 { public static void Main(string[] args) { short a = 10000, b = 10000; short d = unchecked((short)(10000*10000)); Console.WriteLine(d= + d); short c = (short)(a*b); Console.WriteLine(c= + c); short e = checked((short)(a*b)); Console.WriteLine(e= + e); } }
在这段代码中,unchecked 运算符避免了发生编译时错误,否则,下面的语句会产生错误:
short d = unchecked((short)(10000*10000));
下一个表达式在默认情况下是不检查的,因此值会悄悄溢出:
short c = (short)(a*b);
我们可以使用 checked 运算符来强制检查该表达式是否会在运行时溢出:
short e = checked((short)(a*b));
当运行时,赋第一个值给 d & c 会以值 -7936 悄悄溢出,但是当试图使用 checked() 以获得 e 的乘积值时,程序会引发 System.OverflowException 异常。
注意:另外,通过使用命令行编译器开关 (/checked) 或者直接在Visual Studio 中基于每个项目使用此开关,您还可以控制是否检查代码块中的算术溢出。
此运算符确定左边对象的类型是否与右边指定的类型相匹配:
if (objReference is SomeClass) ...
在下面的示例中,CheckType() 方法打印一条消息,描述传递给它的参数的类型:
using System; public class ShowTypes { public static void Main(string[] args) { CheckType (5); CheckType (10f); CheckType ("Hello"); } private static void CheckType (object obj) { if (obj is int) { Console.WriteLine("Integer parameter"); } else if (obj is float) { Console.WriteLine("Float parameter"); } else if (obj is string) { Console.WriteLine("String parameter"); } } }
运行此程序,输出如下:
Integer parameter Float parameter String parameter
sizeof 运算符以指定值类型的字节数返回其大小,如下面的代码所示:
using System; public class Size { public static void Main() { unsafe { Console.WriteLine("The size of short is {0}.", sizeof(short)); Console.WriteLine("The size of int is {0}.", sizeof(int)); Console.WriteLine("The size of double is {0}.",sizeof(double)); } } }
注意,包含 sizeof 运算符的代码放在一个不安全的块中。这是因为 sizeof 运算符被认为是一个不安全的运算符(由于它直接访问内存)。有关不安全代码的更多信息,请参见安全代码和不安全代码。
typeof 运算符返回作为 System.Type 对象传递给它的类的类型。GetType() 方法是相关的,并且返回类或异常的运行时类型。typeof 和 GetType() 都可以与反射一起使用,以动态地查找关于对象的信息,如下面的示例所示:
using System; using System.Reflection; public class Customer { string name; public string Name { set { name = value; } get { return name; } } } public class TypeTest { public static void Main() { Type typeObj = typeof(Customer); Console.WriteLine("The Class name is {0}", typeObj.FullName); // Or use the GetType() method: //Customer obj = new Customer(); //Type typeObj = obj.GetType(); Console.WriteLine("\nThe Class Members\n=================\n "); MemberInfo[] class_members = typeObj.GetMembers(); foreach (MemberInfo members in class_members) { Console.WriteLine(members.ToString()); } Console.WriteLine("\nThe Class Methods\n=================\n"); MethodInfo[] class_methods = typeObj.GetMethods(); foreach (MethodInfo methods in class_methods) { Console.WriteLine(methods.ToString()); } } }
运行此程序,输出如下:
The Class name is Customer The Class Members ================= Int32 GetHashCode() Boolean Equals(System.Object) System.String ToString() Void set_Name(System.String) System.String get_Name() System.Type GetType() Void .ctor() System.String