Visual Studio:针对 Java 开发人员的 C# 编程语言(1)

http://tech.ddvip.com   2007年03月23日    社区交流

本文详细介绍Visual Studio:针对 Java 开发人员的 C# 编程语言(1)

  本文讨论 Java 和 C# 之间的异同点,目的在于当迁移到 .NET 时,让 Java 开发人员掌握所涉及的一些知识。Java 和 C# 之间的主要相似点是:

  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 中的顶层声明

  在 Java 中,我们可以用 package 关键字将类组合在一起。打包的类必须在源文件的第一个可执行的行中使用 package 关键字。接着出现的是需要访问其他包中的类的任何导入语句,之后是类声明,比如:

package ;
import .;
class Customer
{
 ...
}
C# 中的顶层声明

  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# 预处理器指令的完整列表
语言语法
在这一部分中,我们讨论这两种语言之间的相似点和不同点。一些主要的不同点有:

常量声明— 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# 提供了避免在字符串常量中使用转义序列(如代表制表符的“ ”或代表反斜杠字符的“”)的功能。要这样做,可以在为字符串赋值之前使用 @ 符号来声明字符串。下面的示例显示了如何使用转义字符以及如何为字符串赋值:

//Using escaped characters
string path = "\FileShareDirectoryfile.txt";
//Using String Literals
string escapedPath = @"FileShareDirectoryfile.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;

  

  图 1:值类型的内存位置

  则 i 和 j 彼此完全独立,并且分配了不同的内存位置:

  如果我们改变这些变量中的某一个的值,另一个自然不会受到影响。例如,如果我们有一个这样的表达式:

int k = i;

  则变量之间仍然没有联系。也就是说,之后如果我们改变 i 的值,k 还会保留赋值时 i 具有的值。

  然而,引用类型的做法却不同。例如,我们可以这样声明两个变量:

myClass a = new myClass();
myClass b = a;

  现在,因为类是 C# 中的引用类型,所以 a 称为对 myClass 的引用。上面两行中的第一行在内存中创建了 myClass 的一个实例,并且将 a 设置为引用该实例。因此,当我们将 b 设置为等于 a 时,它就包含了对内存中类的引用的重复。如果我们现在改变 b 中的属性,a 中的属性就将反映这些改变,因为这两者都指向内存中的相同对象,如下图所示:

  

  图 2:引用类型的内存位置

  装箱 (Boxing) 和拆箱 (Unboxing)

  这种将值类型转换为引用类型的过程称为装箱。而相反的将引用类型转换为值类型的过程就称为拆箱。如下面的代码所示:

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 和 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 中基于每个项目使用此开关,您还可以控制是否检查代码块中的算术溢出。

is 运算符

  此运算符确定左边对象的类型是否与右边指定的类型相匹配:

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 运算符

  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 和 GetType

  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(" The Class Members ================= ");
   MemberInfo[] class_members = typeObj.GetMembers();
   foreach (MemberInfo members in class_members)
   {
    Console.WriteLine(members.ToString());
   }
   Console.WriteLine(" The Class Methods ================= ");
   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 Name
The Class Methods
=================
Int32 GetHashCode()
Boolean Equals(System.Object)
System.String ToString()
Void set_Name(System.String)
System.String get_Name()
System.Type GetType()

  这为我们显示了从 System.Object 继承的所有类的成员,并且还展示了一种方法,C# 在内部将 get 和 set 属性 accessors 表示为 get_xxx() 和 set_xxx() 方法。

  在下一个示例中,我们使用 GetType() 在运行时查找表达式的类型:

using System;
public class TypeTest
{
 public static void Main()
 {
  int radius = 8;
  Console.WriteLine("Calculated area is = {0}",
               radius * radius * System.Math.PI);
  Console.WriteLine("The result is of type {0}",
               (radius * radius * System.Math.PI).GetType());
 }
}

  此程序的输出告诉我们,结果是 System.Double 类型,选择它是因为System.Math.PI 是这种类型。

Calculated area is = 201.061929829747
The result is of type System.Double

  流程控制

  在这两种语言中,流程控制语句是非常相似的,但是这一部分也会讨论它们的一些细微差别。

分支语句

  分支语句根据特定的条件改变运行时程序执行的流程。

if、else 和 else if

  这些在两种语言中是一样的。

switch 语句

  在两种语言中,switch 语句都提供条件多分支操作。但是有点不同的是,Java 允许您“越过”一个 case 并执行下一个 case,除非您在 case 的末尾使用了 break 语句。然而,C# 需要在每个 case 的末尾都使用 break 或 goto 语句,如果两者都不存在,则编译器会产生下列错误:

Control cannot fall through from one case label to another.

  不过请注意,在没有指定要执行的代码的地方,当 case 匹配时,控制会越过随后的 case。当在 switch 语句中使用 goto 时,我们只能跳至同一 switch 中的另一个 case 块。如果我们想要跳至 default case,我们可以使用“goto default;”,否则,我们需要使用“goto case cond;”,其中 cond 是我们希望跳至的 case 的匹配条件。Java 的 switch 语句的另一个不同之处在于,在 Java 中,我们只能对整数类型使用 switch 语句,而 C# 允许我们对字符串变量使用 switch 语句。

  例如,下面的程序在 C# 中是合法的,但在 Java 中却是不合法的:

switch (args[0])
{
 case "copy":
  ...
  break;
 case "move":
  ...
  goto case "delete";
  break;
 case "del":
 case "remove":
 case "delete":
  ...
  break;
 default:
  ...
  break;
}
goto 的返回

  在 Java 中,goto 是一个没有实现的保留关键字。然而,我们可以使用带有 break 或 continue 标签的语句来达到与 goto 相似的目的。

  C# 允许 goto 语句跳至带有标签的语句。不过请注意,为了跳至一个特定的标签,goto 语句必须在该标签的范围内。换句话说,goto 不可用来跳进一个语句块(不过,它可以跳出一个语句块)、跳出一个类,或退出 try...catch 语句中的 finally 块。不过请注意,在大多数情况下,我们都不鼓励您使用 goto,因为它违反了面向对象编程的良好实践。

循环语句

  循环语句重复指定的代码块,直到满足给定的条件为止。

for 循环

  在两种语言中,for 循环的语法和操作均相同:

for (initialization; condition; expression)
 statement;
foreach 循环

  C# 引入了一种新的循环类型,称为 foreach 循环(类似于 Visual Basic 的 For Each)。foreach 循环允许遍历支持 IEnumerable 接口的容器类中的每一项(例如:数组)。下面的代码说明了如何使用 foreach 语句来输出一个数组的内容:

public static void Main()
{
 int[] arr1= new int[] {1,2,3,4,5,6};
 foreach ( int i in arr1)
 {
  Console.WriteLine("Value is {0}", i);
 }
}

  在 C# 中的数组部分,我们将更详细地介绍 C# 中的数组。

while 和 do...while 循环

  在两种语言中,while 和 do...while 语句的语法和操作均相同:

while (condition)
{
 //statements
}
As usual, don't forget the trailing ; in do...while loops:
do
{
 //statements
}
while(condition);
类基础访问修饰符
C# 中的修饰符与 Java 大致相同,我们将在这一部分介绍其中的一些细微差别。每个类成员或类本身都可以用访问修饰符进行声明,以定义许可访问的范围。没有在其他类中声明的类只能指定 public 或 internal 修饰符,而嵌套的类(如其他的类成员)可以指定下面五个修饰符中的任何一个:

public — 对所有类可见

protected —仅从派生类中可见

private — 仅在给定的类中可见

internal — 仅在相同的程序集中可见

protected internal — 仅对当前的程序集或从包含类中派生的类型可见
public、protected 和 private 修饰符
public 修饰符使得可以从类内外的任何地方访问成员。protected 修饰符表示访问仅限于包含类或从它派生的类。private 修饰符意味着只可能从包含类型中进行访问。
internal 修饰符
internal 项只可以在当前的程序集中进行访问。.NET 中的程序集大致等同于 Java 的 JAR 文件,它表示可以从中构造其他程序的生成块。
protected internal 修饰符
protected internal 项仅对当前程序集或从包含类派生的类型可见。在 C# 中,默认访问修饰符是 private,而 Java 的默认访问修饰符是包范围。
sealed 修饰符
在其类声明中带有 sealed 修饰符的类可以认为是与抽象类完全相反的类 — 它不能被继承。我们可以将一个类标记为 sealed,以防止其他类重写它的功能。自然地,sealed 类不能是抽象的。同时还需要注意,该结构是隐式密封的;因此,它们不能被继承。sealed 修饰符相当于在 Java 中用 final 关键字标记类。
readonly 修饰符
要在 C# 中定义常量,我们可以使用 const 或 readonly 修饰符替换 Java 的 final 关键字。在 C# 中,这两个修饰符之间的区别在于,const 项是在编译时处理的,而 readonly 字段是在运行时设置的。这可以允许我们修改用于在运行时确定 readonly 字段值的表达式。
这意味着给 readonly 字段的赋值可以出现在类构造函数及声明中。例如,下面的类声明了一个名为 IntegerVariable 的 readonly 变量,它是在类构造函数中初始化的:
using System;
public class ReadOnlyClass
{
 private readonly int IntegerConstant;
 public ReadOnlyClass ()
 {
  IntegerConstant = 5;
 }
 // We get a compile time error if we try to set the value of the readonly
 // class variable outside of the constructor
 public int IntMember
 {
  set
  {
   IntegerConstant = value;
  }
  get
  {
   return IntegerConstant;
  }
 }
 public static void Main(string[] args)
 {
  ReadOnlyClass obj= new ReadOnlyClass();
  // We cannot perform this operation on a readonly field
  obj.IntMember = 100;
  Console.WriteLine("Value of IntegerConstant field is {0}",
               obj.IntMember);
 }
}

  注意,如果 readonly 修饰符应用于静态字段,它就应该在该静态字段中进行初始化。

  类的构造函数。

Main() 方法

  每个 C# 应用程序都必须http://msdn.microsoft.com/vstudio/java/gettingstarted/csharpforjava/#Arrays in C#只能包含一个 Main() 方法,Main() 方法指定程序从何处开始执行。注意,在 C# 中,Main() 用大写字母开头,而 Java 使用小写的 main()。

  Main() 只能返回 int 或 void,并且有一个可选的字符串数组参数来表示命令行参数:

static int Main (string[] args)
{
 ...
 return 0;
}

  字符串数组参数可以包含任何传入的命令行参数,这与 Java 中完全一样。因此,args[0] 指定第一个命令行参数,而 args[1] 表示第二个参数,等等。与 C++ 不同的是,args 数组不包含 EXE 文件的名称。

其他方法

  当将参数传递给方法时,它们可能通过值传递,也可能通过引用传递。值参数只提取任何变量的值以在方法中使用,因而调用代码中的变量值不受方法中对这些参数所执行的操作的影响。

  而引用型参数指向在调用代码中声明的变量,因而在通过引用传递时,方法将修改该变量的内容。

通过引用传递

  在 Java 和 C# 中,引用对象的方法参数总是通过引用传递,而基本数据类型参数则通过值传递。

  在 C# 中,所有参数在默认情况下都是通过值传递的。要通过引用进行传递,我们需要指定关键字 ref 或 out 中的一个。这两个关键字的不同之处在于参数的初始化。ref 参数必须在使用前进行初始化,而 out 参数无需在传递前进行显式初始化,并且任何先前的值都会被忽略。

  请注意,当方法将引用类型作为参数使用时,引用本身是通过值传递的。然而,引用仍然指向内存中的同一对象,因此对对象的属性所做的任何改变在方法退出之后将保持不变。但是,因为引用本身是通过值传递的,所以在方法内它应该改为指向一个不同的对象甚至一个新对象,而一旦方法执行完毕,引用就会恢复为指向原来的对象,即使原来的对象是未赋值的也同样如此。

ref 关键字

  当我们希望调用的方法永久性地改变用作参数的变量的值时,我们可以在参数中指定此关键字。发生传递的不是在调用中使用的变量值,而是对变量本身的引用。然后,方法作用于引用,这样,在方法执行过程中对参数所做的更改就保存到用作方法的参数的原始变量中。

  下面的代码用 Add() 方法对此进行了说明,其中,第二个 int 参数是使用 ref 关键字通过引用传递的:

using System;
public class RefClass
{
 public static void Main(string[] args)
 {
  int total = 20;
  Console.WriteLine("Original value of 'total': {0}", total);
  // Call the Add method
  Add (10, ref total);
  Console.WriteLine("Value after Add() call: {0}", total);
 }
 public static void Add (int i, ref int result)
 {
  result += i;
 }
}

  这个简单示例的输出结果表明,对 result 参数所做的改变反映在 Add() 调用所用的变量 total 中:

Original value of 'total': 20
Value after Add() call: 30

  这是因为 result 参数引用了调用代码中的 total 变量所占用的实际内存位置。请注意,类的属性不是一个变量,并且不能直接用作 ref 类型的参数。

  注意,在调用方法时,ref 关键字必须在参数之前,在方法声明中同样如此。

out 关键字

  out 关键字的作用与 ref 关键字非常相似,并且对使用 out 关键字声明的参数所做的修改在方法外是可见的。它与 ref 有两点不同:首先,out 参数的任何初始值在方法中都是忽略的;其次,out 参数必须在方法中赋值:

using System;
public class OutClass
{
 public static void Main(string[] args)
 {
  int total = 20;
  Console.WriteLine("Original value of 'total': {0}", total);
  Add (33, 77, out total);
  Console.WriteLine("Value after Add() call: {0}", total);
 }
 public static void Add (int i, int j, out int result)
 {
  // The following line would cause a compile error
  // Console.WriteLine("Initial value inside method: {0}", result);
  result = i + j;
 }
}

  在该示例中,Add() 方法的第三个参数是用 out 关键字声明的,并且对该方法的调用还需要将 out 关键字用于该参数。输出结果为:

  Original value of 'total': 20Value after Add() call: 110

  因此,总的来说,当您希望方法修改现有的变量时,可以使用 ref 关键字,而使用 out 关键字来返回在该方法中产生的值。当方法为调用代码产生多个结果值时,通常一起使用 out 关键字与该方法的返回值。

使用不确定数目的参数

  通过在声明方法时指定 params 关键字,C# 允许我们发送可变数目的参数。参数列表也可以包含普通参数,但是需要注意,用 params 关键字声明的参数必须放在最后。它采用可变长度数组的形式,并且每个方法只有一个 params 参数。

  当编译器尝试解析一个方法调用时,它查找其参数列表与调用的方法相匹配的方法。如果找不到可以与参数列表匹配的方法重载,但是存在与适当类型的 params 参数匹配的版本,则会调用该方法,并将额外的参数放在一个数组中。

  下面的示例对此进行了演示:

using System;
public class ParamsClass
{
 public static void Main(string[] args)
 {
  Average ("List One", 5,10,15);
  Average ("List Two", 5,10,15,20,25,30);
 }
 public static void Average (string title, params int[] values)
 {
  int Sum = 0;
  Console.Write("Average of {0}: ", title);
  for (int i = 0; i < values.Length; i++)
  {
   Sum += values[i];
   Console.Write(values[i] + ", ");
  }
  Console.WriteLine(": {0}", (float)Sum/values.Length);
 }
}

  在上面的示例中,用整型数组中的 params 参数声明了方法 Average,让我们使用任何数目的参数来调用它。输出结果如下:

Average of List One: 5, 10, 15, : 10
Average of List Two: 5, 10, 15, 20, 25, 30, : 17.5

  注意,如果我们希望允许不同类型的不确定参数,我们可以指定 Object 类型的 params 参数。

属性

  在 C# 中,属性是类、struct,或接口的命名成员,它提供了一种简洁的途径,可以通过所谓的 get 和 set 访问器方法访问私有字段。

  下面的代码片断为类 Animal 声明了一个名为 Species 的属性,它抽象了对名为 name 的私有变量的抽象访问:

public class Animal
{
 private string name;
 public string Species
 {
  get
  {
   return name;
  }
  set
  {
   name = value;
  }
 }
}

  通常,属性与它访问的内部成员有相同的名称,但是属性以大写字母开头(例如上面的示例中的 Name),或者内部成员带有“_”前缀。同时还需要注意 set 访问器中所用的名为 value 的隐式参数 — 这种参数具有基础成员变量类型。

  实际上,访问器在内部表示成 get_X() 和 set_X() 方法,从而与 .NET 语言保持兼容,因为 .NET 语言并不支持访问器(如本文前面的 typeOf 和 GetType() 部分中的屏幕截图所示)。一旦定义好属性,就可以非常容易地获取或设置它的值:

Animal animal = new Animal()
// Set the property
animal.Species = "Lion";
// Get the property value
string str = animal.Species;

  如果属性只有 get 访问器,它就是一个只读属性。如果它只有 set 访问器,它就是一个只写属性。如果两者都有,则它是一个可读写属性。

结构

  C# 支持 struct 关键字,它是源于 C 的另一个项,但是不可用于 Java。可以将 struct 看作是一个轻量级类。它可以采用与类大致相同的方式包含构造函数、常量、字段、方法、属性、索引器、运算符和嵌套类型。structs 不同于类,因为它们不能是抽象的,并且不支持实现继承。结构与类还有一点重要的不同,结构是值类型的,而类是引用类型的。在构造函数的工作方式上,结构也有所不同。特别是,编译器总是提供默认的不带参数的构造函数,并且不允许您替换它。

  在下面的示例中,我们使用 new 关键字并且通过初始化实例的成员初始化了一个 struct:

using System;
public struct CustomerStruct
{
 public int ID;
 public string name;
 public CustomerStruct(int customerID, string customerName)
 {
  ID = customerID;
  name = customerName;
 }
}
class TestClass
{
 public static void Main(string[] args)
 {
  // Declare a CustomerStruct using the default constructor
  CustomerStruct customer = new CustomerStruct();
  Console.WriteLine("Struct values before initialization");
  Console.WriteLine("ID = {0}, Name = {1}", customer.ID,
               customer.name);
  customer.ID = 100;
  customer.name = "Robert";
  Console.WriteLine("Struct values after initialization");
  Console.WriteLine("ID = {0}, Name = {1}", customer.ID,
               customer.name);
 }
}

  当我们编译并运行上面的代码时,它的输出显示,该结构的变量在默认情况下是已初始化的。int 变量初始化为 0,而字符串变量初始化为空字符串:

  初始化前的 struct 变量

ID = 0, Name =

  初始化后的 truct 值

ID = 100, Name = Robert

  注意,当我们使用另一种表示法(CustomerStruct customer)声明 customer 时,它的成员变量将不被初始化,因此,如果试图在为它们赋值前使用它们,将会产生编译时错误。

C# 中的数组

  数组是具有相同数据类型的项的有序集合,通过数组名以及所期望的项相对于数组开始位置的偏移量可以访问数组。与 Java 相比,在 C# 中声明和使用数组的方式有一些重要的不同,我将在这一部分中对此进行介绍。

一维数组

  一维数组以线性方式存储了固定数目的项,它仅仅需要一个索引值就可以确定任何一项。

  在 C# 中,数组声明中的方括号必须紧跟数据类型,而不可以像在 Java 中一样出现在变量名的后面。因此,可以使用下面的语法来声明整数类型的数组:

int[] MyArray;

  而下面的声明在 C# 中是无效的:

int MyArray[];

  一旦声明了数组,就可以使用新的关键字来设置它的大小,这与 Java 中是完全一样的:

int[] MyArray;      // declares the array reference
MyArray = new int[5];  // creates a 5 element integer array

  然后,我们就可以使用与 Java 完全相同的语法来访问一维数组中的元素,注意 C# 数组的索引也是从零开始的:

MyArray [4]     // accesses the last element in the array初始化

  可以使用与 Java 相同的语法在创建时对数组元素进行初始化:

MyArray = new int[5] {1, 2, 3, 4, 5};

  与 Java 不同,初始化器的数目必须与数组大小完全匹配。 我们可以利用这一特性在一行中声明和初始化 C# 数组:

 int[] TaxRates = {0, 20, 23, 40, 50};

  此语法创建了一个大小与初始化器的数目相等的数组。

程序循环中的初始化

  C# 中初始化数组的另一种方法就是使用 foreach 循环。下面的循环将数组中的每个元素都设置为零:

int[] MyLittleArray = new int[5];
foreach (int i in MyLittleArray)
{
 MyLittleArray[i] = 0;
}
交错数组

  C# 和 Java 都支持创建交错或者说非矩形的数组,其中的每一行都包含不同数目的列。例如,下面的交错数组的第一行有四项,而第二行有三项:

int[][] JaggedArray = new int[2][];
JaggedArray[0] = new int[4];
JaggedArray[1] = new int[3];
多维数组

  C# 允许我们创建规则的多维数组,它可以看作是相同类型的值的矩阵。虽然 Java 和 C# 都支持交错的数组,但是 C# 还支持多维数组或数组的数组。我们将马上介绍交错数组。

  使用下面的语法,我们可以声明一个多维矩形数组:

int[,] My2DIntArray;
float[,,,] My4DFloatArray;

  其中,My2DintArray 是可以借此访问每个元素的名称。

  注意, int[][] My2DintArray; 行在 C# 中有不同的含义,我们很快就会明白这一点。

  一旦声明了数组,我们就可以这样给它分配内存:

int[,] My2DIntArray;      // declares array reference
My2DIntArray = new int[5,4];  // allocates space for 5x4 integers

  然后,可以使用下面的语法来访问数组的元素:

My2DIntArray [4,3] = 906;

  因为数组是从零开始的,所以这将第四行第五列中的元素(右下角)设置为 906。

初始化

  通过下面的任何一种方法,都可以在一行中创建、设置和初始化多维数组:

int[,] intArray = {  {1,2,3},
           {4,5,6}  };
int[,] intArray = new int [2,3] {  {1,2,3},
                  {4,5,6}  };
int[,] intArray = new int [,] {  {1,2,3},
                  {4,5,6}  };
程序循环中的初始化

  数组中所有的元素都可以使用嵌套的循环进行初始化,如下所示:

int[,] intArray = new int[5,4];
foreach (int i in intArray)
{
 foreach (int j in intArray[])
 {
  j = 0;
 }
}
System.Array 类

  在 .NET 中,数组是作为 System.Array 类的实例实现的。此类提供了几个有用的方法,例如 Sort() 和 Reverse()。

  下面的程序说明了使用这几个方法是多么的容易。首先,我们使用 Array 类的 Reverse() 方法来使数组的元素反向,然后,我们使用 Sort() 方法对它们进行排序:

using System;
public class ArrayMethods
{
 public static void Main()
 {
  // Create string array of size 5
  string[] EmployeeNames = new string[5];
  Console.WriteLine("Enter five employee names:");
  // Read 5 employee names from user
  for(int i=0;i<5;i++)
  {
    EmployeeNames[i]= Console.ReadLine();
  }
  // Print the array in original order
  Console.WriteLine(" ** Original Array **");
  foreach(string EmployeeName in EmployeeNames)
  {
    Console.Write("{0} ", EmployeeName);
  }
  //print the array in reverse order.
  Console.WriteLine(" ** Values in Reverse Order **");
  System.Array.Reverse(EmployeeNames);
  foreach(string EmployeeName in EmployeeNames)
  {
    Console.Write("{0} ", EmployeeName);
  }
  
  //print the array in sorted order.
  Console.WriteLine(" ** Values in Sorted Order **");
  System.Array.Sort(EmployeeNames);
  foreach(string EmployeeName in EmployeeNames)
  {
    Console.Write("{0} ", EmployeeName);
  }
    
 }
}

  下面是此程序的一些典型输出:

Enter five employee names:
Luca
Angie
Brian
Kent
Beatriz
** Original Array **
Luca Angie Brian Kent Beatriz
** Values in Reverse Order **
Beatriz Kent Brian Angie Luca
** Values in Sorted Order **
Angie Beatriz Brian Kent Luca
继承和派生类
通过创建一个从现有类派生的新类,我们可以扩展现有类的功能。派生类继承了基类的属性,并且我们可以在需要时添加或重写方法和属性。
在 C# 中,继承和接口实现都通过 : 运算符来定义,这等同于 Java 中的扩展和实现。注意,基类应该一直在类声明的最左边。
同 Java 一样,C# 不支持多重继承,这意味着类不能从多个类中继承。然而,我们可以为此目的而采用与 Java 相同的方式使用接口,正如我们在下一部分中将看到的。
下面的代码定义了一个名为 Point 的类,它有两个私有成员变量 x 和 y,表示点的位置。这些变量可以分别通过名为 X 和 Y 的属性来访问:
public class Point
{
 private int x, y;
 public Point()
 {
  x = 0;
  y = 0;
 }
 public int X
 {
  get
  {
   return x;
  }
  set
  {
   x = value;
  }
 }
 public int Y
 {
  get
  {
   return y;
  }
  set
  {
   y = value;
  }
 }
}

  我们将从 Point 类派生一个新类(比如说名为 ColorPoint),如下所示:

public class ColorPoint : Point

  于是 ColorPoint 继承了基类的所有字段和方法,我们可以根据需要向派生类中添加新的字段和方法来提供额外的特性。在这个示例中,我们添加了一个私有成员和一些访问器来为点增加颜色:

using System.Drawing;
public class ColorPoint : Point
{
 private Color screenColor;
 public ColorPoint()
 {
  screenColor = Color.Red;
 }
 public Color ScreenColor
 {
  get
  {
   return screenColor;
  }
  set
  {
   screenColor = value;
  }
 }
}

来源:microsoft    责编:豆豆技术应用

正在加载评论...