1. 默认的命名空间 命名空间是C#类、接口、委托、枚举和其他类型的一种逻辑上的组合。如果您没有定义自己的命名空间,代码会自动放入一个没有名称的全局命名空间中。命名空间的第一部分通常是一个公司或组织的名称,第二部分应该是相关技术的名称,后面跟可选的特性和设计名。比如,如果您在Acme Engines Corporation工作,您可以这样定义您好的命名空间:AcmeEngines.FuelInjector.Diesel。 2. 类的默认访问控制符internal 类有四种访问属性:internal、private、protected和public。在命名空间中声明的类只能有pubic或internal访问属性。没有指定访问修饰符时,类的访问类型默认为internal,即该类只能从同一个程序集(assembly)的其它类中访问。 让我们来看看当类不指定访问修饰符时,其它程序集中的类对它的访问情况。首先在名为Quad.cs文件中定义一个Quad类,然后在名为Test.cs中定义一个Test类。我们试图在Test类中使用Quad类的一个实例。两个类的定义如下: Quad.cs文件的内容为: using System; class Quad { double width, height; public Quad(double w, double h) { width = w; height = h; } public double GetArea() { return width * height; } } Test.cs文件的内容为: using System; public class Test { public static void Main() { Quad q = new Quad(2.0, 3.0); Console.WriteLine("Area is {0}", q.GetArea()); } } 为了测试,我们将Quad.cs文件单独编译成.dll文件。方法是: 第一步:把Quad.cs文件复制到vs.net安装的目录下面,即与C#编译器控制台csc.exe处于同一目录。笔者的系统装在D盘,所以把Quad.cs文件复制到D:\WINDOWS\Microsoft.NET\Framework\v1.1.4322目录下面。 第二步:打开命令窗口->输入cmd到控制台->cd D:\WINDOWS\Microsoft.NET\Framework\v1.1.4322转到vs.net安装的该目录下。 第三步:执行csc命令csc /target:library Quad.cs把程序Quad.cs编译为一个库文件Quad.dll 对文件Test.cs首先作第一步和第二步处理。由于Test类使用了Quad类的一个实例。要访问该类,使用下面的语法编译Test类: csc /Test.cs /reference:Quad.dll。 但是,您会得到下面的错误信息: Test.cs(7,9):error CS0122:不可访问“Quad”,因为它受保护级别限制。 Test.cs(8,42):error CS0246:找不到类型或命名空间名称“q”(是否缺少using指令或程序集引用?) 这是因为Quad类和Test类位于不同的程序集中,而且Quad类是内部访问方式,所以Test类不能访问它。要解决这个问题,用public修改符定义Quad类就可以了: using System; public class Quad { double width, height; public Quad(double w, double h) { width = w; height = h; } public double GetArea() { return width * height; } } 3. 三种方法创建一个类实例 类的实例是一个引用类型的变量。因此,对象存放在堆中。 实例化对象主要有三种方法: (1) 使用new关键字。new关键字的作用是调用一个称为构造函数的特殊函数。要创建一个Test类的实例,可输入:Test mytest=new Test(); 这个语法实际上是把两个语句合二为一。语句的第一部分Test mytest创建一个Test类型的引用,名为mytest。语句的第二部分=new Test();创建一个与引用相关的Test对象,并调用没有参数的Test类的构造函数初始化该对象。 (2) 有些类可能没有可访问的构造函数,这种情况下就不能用构造函数来实例化该对象。这些对象的实例一般是在.NET Framework的方法内部创建,这些方法返回对类实例的引用。例如:File类的Create()方法(一个静态方法)返回对FileStream对象的引用。可以用语法:FileStream fs=File.Create("data.inp");对一个名为data.inp的文件实例化一个FileStream对象。 (3) 实例化对象还有一种方式—使用反射。我们可以使用System.Type类动态检索具体类型的ConstructorInfo对象,并调用它检索类的一个实例。例如:给定一个ReflectTest类,它有一个公有的、无参数的构造函数: public class ReflectTest { public ReflectTest() { } } 可使用下面的代码实例化该类: using System; using System.Reflection; public class ReflectTest { public ReflectTest() { } } public class Application { //Get the Type object for the ReflectTest class Type t=Type.GetType("ReflectTest"); //Get the parameterless constructor for the class ConstructorInfo ctor=t.GetConstructor(System.Type.EmptyTypes); //Invoke the constructor with a 'null' parameter ReflectTest test=(ReflectTest)ctor.invoke(null); } 4. 将构造函数声明为私有 如果没有显式定义构造函数,系统将提供一个默认的、不带参数的public构造函数,它将把数据成员初始化为默认值。如果希望拒绝对该构造函数的仅有访问(例如:如果只希望通过工厂类实例化该类),可以用一个不同的访问级别声明这个构造函数。如果定义了一个无参的private构造函数,我们将阻止该类被实例化(这种做法可以针对这样的类:它只有静态成员,而又不能声明为abstract,因为我们想把它像System.Math类一样声明为sealed)。 5. 调用基类的构造函数 当创建类的实例时,它的创建工作包括它的基类的创建。从基类继承的任何数据成员必须进行初始化。派生类的构造函数的一个操作是从它的直接基类中调用一个构造函数。这个操作可以使用基类的默认(无参数)构造函数隐式进行,也可以让派生类的构造函数显式调用基类的构造函数。如果类没有定义无参构造函数,就必须显式地调用一个基类的构造函数。方法是使用 :base 关键字。如: using System; public class Quad { double width, height; public Quad(double w, double h) { width = w; height = h; } public double GetArea() { return width * height; } } public class NamedQuad : Quad { string name; public NamedQuad(double w, double h, string str) : base(w, h) { name = str; } public string GetName() { return name; } } public class BaseConstDemo { public static void Main() { NamedQuad nq = new NamedQuad(2.0, 3.0, "Harry"); Console.WriteLine("Area of {0} is {1}", nq.GetName(), nq.GetArea()); } } 运行结果: Area of Harry is 6 这个例子中一个有趣的地方是如果我们是这样定义NamedQuad类构造函数的: public NamedQuad(double w, double h, string str) { width = w; height = h; name = str; } 程序不会通过编译。这是因为NamedQuad构造函数将试图从Quad类调用一个无参数的构造函数,但Quad类没有定义一个无参数的构造函数。 6. 结构和类的区别 结构和类的主要不同在于: (1) 结构存放在栈中并按值传递。类的对象放在堆中,按引用传递。和类的对象相比,结构具有性能上的优势。原因之一是值类型的分配快于引用类型。原因之二是存放在栈中的值一离开作用域就立即被收回,不用等待垃圾收集器来处理。但是,结构作为参数传递给方法前会复制它的一个完整的副本,所以只能用来表示小的数据结构。而当引用类型传递给方法时,传递的只是对对象的引用。 (2) 结构既不能定义无参数的构造函数,又不能定义析构函数。 (3) 结构的隐式类型是sealed类型,不能被继承。虽然它们可以提供一个或多个接口的实现。 7. 接口默认的访问方式是internal 接口为类或结构提供了蓝图。它是一个能声明属性、索引器、事件和方法的编程构造。它不为这些成员提供实现,只提供定义。实现接口的数据类型必须提供接口成员的实现。在命名空间中声明的接口可以被授予public或internal访问类型,默认的访问方式是internal。接口中声明的成员隐式地为public和abstract,它们声明时没有实现,没有访问修饰符。C#的约定是接口名以一个大写的字母“I”开始。 8. 方法中参数的修饰符(params/ref/out) 参数按值或按引用传递。未修饰的参数按值传递,方法中变量的改变不会影响到最初的变量。但是,因为对象变量是存放在堆中的,存放在栈中的只是该位置的一个引用,所以传递给方法的是这个内存地址,而不是实际的对象,对对象的任何改变将会反映在最初的变量中。 引用参数按引用传递给方法。这意味着传递给方法的是对对象的一个引用。在方法内部对引用类型的改变都会影响到最初的变量。 按值传递的引用类型参数和按引用传递的参数之间的差别是很微小的。但在处理字符串时这种差异则很明显。 8.1 params关键字 params关键字指明一个输入参数将被看作一个参数数组。参数数组将包含数量不定的指定类型的参数。因此params关键字用于可变的参数列表行为。params关键字只能用于参数列表中的最后一个参数。 示例:params关键字 ParamsDemo类定义了Process()方法,它只打印一个Strings数组。如果没有使用params关键字,您必须在调用该方法前创建并加载Strings数组: using System; public class ParamsDemo { public static void Main() { String[] s = new String[3]; s[0] = "Jackson"; s[1] = "Zachary"; s[2] = "Erica"; ParamsDemo.Process(s); } public static void Process(String[] str) { for (int i = 0; i < str.Length; ++i) { Console.WriteLine(str[i]); } } } 下面是在参数列表中使用params关键字时的程序: using System; public class ParamsDemo { public static void Main() { ParamsDemo.Process("Jackson","Zachary","Erical"); } public static void Process(params String[] str) { for (int i = 0; i < str.Length; ++i) { Console.WriteLine(str[i]); } } } 运行结果: Jackson Zachary Erica 8.2 ref关键字 ref关键字让一个值类型的输入参数按引用传递。在方法中对变量的任何修改也将改变最初变量的值。ref关键字放在参数列表中变量类型的前面。在调用方法时也必须使用ref关键字。 ref关键字的一个作用是取消一个方法只能返回一个值的限制。 引用参数既传入方法,又从方法中传出,在传入前必须初始化。 示例:ref关键字 例子中定义了一个静态Scale(),它按指定量改变两个浮点数。两个浮点数使用ref关键字被传递给方法。当它们的值在Scale()方法内改变时,它们将在方法返回时保持这些变化。注意调用Scale()时ref关键字的使用: using System; public class Demo { public static void Scale(double scale, ref double a, ref double b) { a *= scale; b *= scale; } public static void Main() { double d1, d2; d1 = 2.0; d2 = 3.0; Demo.Scale(1.5, ref d1, ref d2); Console.WriteLine("d1 is {0}", d1); Console.WriteLine("d2 is {0}", d2); Console.ReadKey(); } } 运行结果: d1 is 3 d2 is 4.5 我们前面说过,引用类型按值传递,但这个值是对象在内存中存储的地址。因此对对象所做的任何修改将反映在调用作用域中的变量上。然而,字符串有一些不同。因为字符串在C#中是不可变的,每当修改字符串时就会创建一个新的字符串对象。这意味着最初的变量不会受到影响(它仍将引用最初的String对象)。相似的,如果使用new关键字显式创建一个新的对象并将一个值参数指向它,这不会影响到调用作用域中的原始变量。 引用参数在这里就不一样了(引用参数是有效的输入/输出参数),如果参数的值在方法体内修改,或者如果参数指向一个不同的对象,在方法外部的原始变量也将被更新并指向新的引用。 示例:按引用传递字符串 为了演示引用参数和引用类型的值参数之间的不同,下面的代码调用了两个方法,每个方法带一个字符串参数并在方法体内修改这个字符串。第一个方法接受一个值参数,第二个方法使用ref关键字接受一个引用参数: using System; public class StringDemo { static void DoSomethingByValue(string s) { s="A different string"; } static void DoSomethingByReference(ref string s) { s = "Another string"; } static void Main(string[] args) { string s = "A string"; Console.WriteLine(s); DoSomethingByValue(s); Console.WriteLine(s); DoSomethingByReference(ref s); Console.WriteLine(s); Console.ReadKey(); } } 运行结果: A string A string Another string 8.3 out关键字 在C#中,变量在使用前总是必须初始化。如果定义了一个方法,它将把一个值赋给一个输入参数,该输入变量在传递给方法前,仍必须被赋给一个哑值。使用out关键字可以避免这个无意义的步骤。 使用out关键字修改的输入参数成为输出参数,能在初始化前传递给方法。out关键字放在方法声明语句中变量类型的前面。方法调用中也必须使用out关键字。 输出参数在方法返回前必须被赋给一个值。 示例:out关键字 注意即使返回到Main()方法后,整型输出参数也保持着在SetString()方法中赋给它的值。这是因为输出参数使用的存储位置和方法调用中人微言轻参数的变量是相同的。 using System; public class OutDemo { public static void SetString(out string str, out int i) { str = "Happy New Year"; i = 4; } public static void Main() { String s; int i; OutDemo.SetString(out s, out i); Console.WriteLine(s); i += 2; Console.WriteLine(i); Console.ReadKey(); } } 运行结果: Happy New Year 6