时间:2022-11-09 09:13:41 | 栏目:.NET代码 | 点击:次
类和结构实际上都是创建对象(实例)的模版,每个对象都包含数据,并提供了处理和访问数据的方法。
类定义了类的每个对象可以包含什么数据和功能。
class PhoneCus { public const string DaySend = "Mon"; public int CusId; }
结构与类的区别是它们在内存中的存储方式,访问方式和它们的一些特性(稍后详细介绍它们的区别)。
较小的数据类型使用结构可提高性能,在语法上,比较类似,主要区别是使用关键字struct代替class来声明结构。
struct PhoneCusStruct { public const string DaySend = "Mon"; public int CusId=; }
对于类和结构,都是用new来声明实例:这个关键字创建对象并对其进行初始化。
PhoneCus myCus = new PhoneCus(); PhoneCusStruct myCus2 = new PhoneCusStruct();
上面的例子,类和结构的字段值都默认0.
类中的数据和函数称为类的成员(数据成员和函数成员)。
数据成员是包含类的数据————字段,常量和事件的成员。数据成员可以是静态数据。类成员总是实例成员,除非用static显示声明。
函数成员提供了操作类中数据的某些功能,包括方法,属性,构造函数,终结器,运算符以及索引。
*C#区分函数和方法。C#中函数包含上述提到的。
参数可以通过引用或值传递给方法。在变量通过引用传递给方法时,被调用的方法得到的就是这个变量,准确的説就是指向内存中变量的指针。所以在方法内对变量进行的任何改变在方法退出后仍然有效。
而如果变量通过值传递给方法,被调用的方法得到的是变量的一个相同副本,也就是说,在方法退出后,对变量的修改会丢失。
对于复杂的数据类型,按引用传递的效率更高,因为在按值传递时,必须复制大量的数据。
注意字符串的行为方式有所不同,因为字符串是不可变的,所以字符串无法采用一般引用类型的行为方式。在方法调用中,对字符串所做的改变都不会影响原始字符串。
像上面所説,值类型通过值传递变量是默认的。但也可以迫使值参数通过引用传递给方法。为此要使用ref关键字。这样该方法对变量所做的任何改变都会影响原始值。
static void SomeFunction(int[] ints,ref int i) { ints[0] = 100; i = 100; }
在调用该方法的时候,必须添加ref关键字。
SomeFunction(ints, ref i);
C#要求变量在被引用前必须用一个初始值进行初始化。但使用out关键字来初始化可以简化C# 编译器所坚持的输入参数的初始化。
在方法的输入参数前加上out前缀时,传递给该方法的变量可以不初始化。而且该变量通过引用传递,所以在从被调用的方法中返回时,对应方法对该变量进行的任何改变都会保留下来。
在调用该方法时,仍需要使用out关键字:
static void SomeFunction(int[] ints,out int i) { ints[0] = 100; i = 100; } SomeFunction(ints, out i);
参数一般需要按定义的顺序传递给方法。命名参数允许按任意顺序传递。
string FullName(string firstName,string lastName) { renturn firstName+" " +lastName; }
调用方法:
FullName("John","Doe"); FullName(lastName:"Doe",firstName:"John");
参数也可以是可选的。必须为可选参数提供默认值。可选参数还必须是方法定义的最后一个参数。
void TestMethod(int notOption,int option = 10) { Console.WriteLine( notOption + option); }
C#支持方法的重载————方法的几个版本有不同的签名(方法名相同,但参数的个数和/或类型不同)。
class MathTest { public int Value; public int GetSquare() { return Value*Value; } public int GetSquare(int x) { return x*x; } }
重载方法在参数方面的一些限制:
两个方法不能仅在返回类型上有区别;
两个方法不能仅根据参数是声明为ref还是out来区分。
在任何语言中,对于方法重载,如果调用了错误的重载方法,就有可能出现运行错误。(后面讨论如何避免这些错误)。
属性是一个方法或一对方法,在客户端看来,它是一个字段。
public string SomeProperty { get { return "value"; } set { //设置属性值 } }
get访问器不带任何参数,且必须返回属性声明的类型。也不应为set访问器指定任何显示参数,编译器会
假定它带一个参数,器类型也许属性相同,并表示为value.
private int age public int Age { get { return age; } set { age = valeu; } }
注意所用的命名约定,采用C#的区分大小写模式,使用相同的名称,但公有属性采用大写形式命名,如果存在一个等价的私有字段,则采用小写形式命名。
一些开发人员喜欢使用把下划线作为前缀的字段名,如_age,这会为识别字段提供极大的便利。
在属性定义中省略set访问器,就会创建只读属性。这样客户端代码只可以读取该属性的值,但不能设置值。
private int age public int Age { get { return age; } }
同样在属性定义中省略get访问器,就会创建只写属性。
C#允许给属性的gei和set访问器设置不同的访问修饰符,所以属性可以有公有的get访问器和受保护的set访问器。
在gey和set访问器中,必须有一个具有属性的访问级别(公有)。
如果属性的set和get访问器中没有任何逻辑,就可以使用自动实现的属性。这种属性会自动实现后背成员变量。
public int Age { get; set; }
不需要声明private int age;,编译器会自动创建它。
使用自动实现的属性,就不能在属性设置中验证属性的有效性。但必须有两个访问器,不能把属性设置为只读或只写。
public int Age { get;//报错 } 但是,每个访问器的访问级别可以不同, public int Age { get; private set; }
声明基本构造函数就是声明一个与包含的类同名的方法,但该方法没有返回值。
public class MyClass { public MyClass() { } // }
一般情况下,如果没有提供任何构造函数,编译器会在后台创建一个默认的构造函数。这是一个基本的构造函数,它只能把所有的成员字段初始化为标准的默认值。这通常就足够了,否则需要编写自己的构造函数。
构造函数的重载与其它方法的规则相同。可以为构造函数提供任意多的的重载,只要它们的签名有明显区别。
public class MyClass { public MyClass() { } public MyClass(int i ) { / / } // }
如果提供了带参数的构造函数,编译器就不会自动提供默认的构造函数。只有在没有定义任何构造函数的时候,编译器才会自动提供默认的构造函数。
public class MyNum { private int number; public MyNum(int number) { this.number =number; } }
一般使用this关键字区分成员字段和同名的参数。
如果试图使用无参数的构造函数实例化对象就会报错:
MyNum num = new MyNum();//报错
可以把构造函数定义为private或protected,这样不相关的类就不能访问它们:
public class MyNum { private int number; private MyNum(int number) { this.number =number; } }
上述例子没有为MyNum定义为任何公有或受保护的构造函数。这就使MyNum不能使用new运算符在外部代码中实例化,但可以在MyNum类中编写一个公有静态属性或方法,以实例化该类。
这在下面两种情况下受有用的:
类仅用作某些静态成员或属性的容器,因此永远不会实例化它。
希望类仅通过某个静态成员函数来实例化。
C#可以给类编写无参数的静态构造函数。这种构造函数只执行一次,而前面的构造函数是实例构造函数,只要创建类的对象,就会
执行它。
class MyClass { static MyClass() { } }
编写静态构造函数的一个原因是,类有一些静态字段或属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性。
.NET运行库不能确保什么时候执行静态构造函数,所以不能把要求在某个特定时刻执行的代码放在静态构造函数中。也不能预计不同类的静态构造函数按照什么顺序执行。但是可以确保静态构造函数最多运行一次,就在代码引用类之前调用它。
在C#中,通常在第一次调用类的任何成员之前执行静态构造函数。
注意,静态构造函数没有访问修饰符,其它C#代码从来不调用它,但在加载类时,总是由.NET运行库调用它,所以像public,private这样的访问修饰符就没有任何意义。出于同样原因,静态构造函数不能带任何参数,一个类也只能有一个静态构造函数。很显然,静态构造只能访问累的静态成员,不能访问类的实例成员。
无参数的实例构造函数与静态构造函数可以在同一个类中同时定义。虽然参数列表相同,但这并不矛盾,因为在加载类的时候执行静态构造函数,在创建实例时执行实例构造函数,所以何时执行哪个构造函数不会有冲突。
如果任何静态字段有默认值,就在调用静态构造函数之前指定它们。
下面演示静态构造函数的用法:
class MainEntryPoint { static void Main() { Console.WriteLine("UserPreference:BackColor is " + UserPreference.BackColor.ToString()); } } class UserPreference { public static readonly Color BackColor; static UserPreference() { BackColor = Color.Red; } private UserPreference() { } }
该静态变量在静态构造函数中进行初始化。
有时,在一个类中有几个构造函数,这些构造函数包含一些共同的代码。
class Car { private string des; private int nWheels; public Car(string des,int nWheels) { this.des = des; this.nWheels = nWheels; } public Car(string des) { this.des = des; this.nWheels = 4; } }
这两个构造函数初始化了相同的字段,显然最好把所有的代码放在一个地方。C#有一个特殊的语法,称为构造函数初始化器,可以实现这个目的。
class Car { private string des; private int nWheels; public Car(string des,int nWheels) { this.des = des; this.nWheels = nWheels; } public Car(string des):this(des,4) { } }
这里,this关键字仅调用参数最匹配的那个构造函数。构造函数初始化器在构造函数的函数体之前执行。
C#构造函数初始化器可以包含对同一个类的另一个构造函数的调用,也可以包含对直接基类的构造函数的调用,使用同样的语法,但应用base关键字代替this.初始化器中不能有多个调用。
常量是一个包含不能修改的值的变量。但常量不必满足所有的要求。有时需要一些一些变量,其值不应改变,但在运行之前其值是未知的。C#为这种情形提供了另一种类型的变量:只读字段(readonly)。
readonly关键字比const灵活得多,允许把一个字段设置为常量,但可以执行一些计算,以确定它得初始值。
其规则是可以在构造函数中给只读字段赋值,但不能在其它地方赋值。只读字段还可以是一个实例字段,类的每个实例可以有不同得值。
与const不同,如果要把只读字段设置为静态,就必须显示得声明它。
var关键字用于表示隐式类型化得变量。var和new关键字一起使用时,可以创建匿名类型。
匿名类型只是一个继承自Object且没有名称的类。
var caption = new {FirstName = "John",LastName="Doe"};
这会生成一个包含FirstName,LastName属性的对象。
创建另一个对象:
var doctor = new {FirstName = "James",LastName="Mc"};
caption和doctor的类型就相同,可以设置caption = doctor
如果设置的值来自于另一个对象,就可以简化初始化器。
var doctor = new {caption.FirstName,caption.LastName};
这些对象的类型名未知。编译器为类型“伪造”了一个名称,但只有编译器才能使用它。
如果仅需要一个小的数据结构,此时类提供的功能多余我们需要的功能,由于性能原因,最好使用结构。
结构是值类型,它们存储在栈中或存储为内联(inline)(如果它们是存储在堆中的另一个对象的一部分),其生存期的限制与简单的数据类型一样。
结构实际上是把数据项组合在一起,有时大多数字段都声明为public。严格来说,这与编写.net代码的规则相反(字段应总是私有的(除const字段外),并由公有属性封装)。但是,对于简单的结构,公有字段是可以接受的编程方式。
虽然结构是值类型,但在语法上可以把它当作类来处理。
struct PhoneCusStruct { public const string DaySend = "Mon"; public int CusId=0; } PhoneCusStruct phoneCusStruct = new PhoneCusStruct(); phoneCusStruct.CusId=3;
因为结构是值类型,所以new运算符与类和其它引用类型的工作方式不同。new运算符并不分配堆中的内存,而只是调用相应的构造函数,根据传送给它的参数,初始化所有的字段。
对于结构编写下面的代码是合法的:
PhoneCusStruct phoneCusStruct; phoneCusStruct.CusId=3;
结构遵循其它数据类型都遵循的规则:在使用前所有的元素都必须进行初始化。在结构上调用new运算符,或者给所有的字段分别赋值,结构就完全初始化了。
如果结构定义为类的成员字段,在初始化包含的对象时,该结构会自动初始化为0.
结构是会影响性能的值类型,但根据使用结构的方式,这种影响可能是正面的,也可能是负面的。正面的影响是为结构分配内存时,速度很快,因为它们将内联或保存在栈中。在结构超出了作用域被删除时,速度也很快,不需要等待垃圾回收。负面影响是,只要把结构作为参数来传递或者把一个结构赋予另一个结构,结构的内容就会被复制,而对于类只复制引用。这样就会有性能损失,根据结构的大小,性能损失也不同。
注意,结构主要用于小的数据结构。当把结构作为参数传递给方法时,应把它作为ref参数传递,以避免性能损失(这样只传递了结构在内存中的地址)。
结构不能从一个结构中继承。唯一的例外是对应的结构(和其它类型一样)最终派生于类System.Object。因此结构也可以访问Object的方法。
在结构中也可以重写Object中的方法——如ToString()方法。
结构的继承链是:每个结构派生于System.ValueType类,System.ValueType类有派生于System.Object。ValueType并没有给Object添加任何成员,但提供了一些更适合结构的实现方法。
注意,不能为结构提供其它基类。
为结构定义构造函数的方式与类的方式相同,但不允许定义无参数的构造函数。因为在一些罕见的情况下,.NET运行库不能调用用户提供的自定义无参数构造函数,因此Microsoft干脆采用禁止在C#的结构内使用无参数的构造函数。
默认构造函数会隐式的把字段初始化,即使提供了其它带参数的构造函数,也会先调用它。提供字段的初始值也不能绕过默认构造函数。下面代码会编译错误:
struct PhoneCusStruct { public int CusId =0; }
如果PhoneCusStruct声明为一个类,就不会报错了。
另外,可以像类那样为结构提供Close()或Dispose()方法。
在应用程序代码内实例化一个类或结构时,只要有代码引用这个对象,就会形成强引用。这意味着垃圾回收器不会清理这个对象使用的内存,一般而言这是好事,因为可能需要引用这个对象,但是如果这个对象很大,而且不经常访问。这个时候可以创建对象的弱引用。
弱引用允许创建和使用对象,但在垃圾回收器运行时,就会回收对象并释放内存。由于存在潜在的Bug和性能问题,一般不会这么做,但在特定情况下使用是合理的。
弱引用使用WeakReference类创建。因为对象可能在任意时刻被回收,所以引用该对象前必须确认它的存在。
class MainEntryPoint { static void Main() { // Instantiate a weak reference to MathTest object WeakReference mathReference = new WeakReference(new MathTest()); MathTest math; if(mathReference.IsAlive) { math = mathReference.Target as MathTest; math.Value = 30; Console.WriteLine( "Value field of math variable contains " + math.Value); Console.WriteLine("Square of 30 is " + math.GetSquare()); } else { Console.WriteLine("Reference is not available."); } GC.Collect(); if(mathReference.IsAlive) { math = mathReference.Target as MathTest; } else { Console.WriteLine("Reference is not available."); } } } // Define a class named MathTest on which we will call a method class MathTest { public int Value; public int GetSquare() { return Value*Value; } public static int GetSquareOf(int x) { return x*x; } public static double GetPi() { return 3.14159; } }
partial关键字允许把类,结构,接口放在多个文件中。
partial关键字的用法:把partial放在class,struct,interface前面即可。
如果声明类时使用了下面的关键字,这些关键字就必须应用于同一个类的所有部分:
public,private,protected,internal,abstract,sealed,new,一般约束
在把部分类编译后,类的成员和继承等会合并。
如果类只包含静态的方法和属性,该类就是静态的。静态类在功能上与使用私有静态构造函数创建的类相同。都不能创建静态类的实例。
使用static关键字,编译器可以检查用户是否给该类添加了实例成员。如果是,就会生成一个编译错误。这可以确保不创建静态类的实例。
static class PhoneCusStruct { public static void GetPhene() { } }
调用:PhoneCusStruct.GetPhene();
前面提到,所有的.NET类都派生自System.Object类.实际上,如果在定义类的时候没有指定基类,编译器就会自动假定这个类派生自Object类。其实际意义在于,除了自己定义的方法和属性等外,还可以访问Object定义的许多公有的和受保护的成员方法。这些方法可用于自己定义的其它类中。
System.Object的方法:
如果有类的源码,继承就可以给对象添加方法。但如果没有源代码,则可以使用扩展方法,它允许改变一个类,但不需要该类的源代码。
扩展方法是静态方法,它是类的一部分,但实际上没有放在类的源代码中。假定PhoneCusStruct类需要一个Add()方法,但不能修改源代码,就可以创建一个静态类,把Add()方法添加为一个静态方法:
public static class PhoneExtension { public static void Add(this PhoneCusStruct phoneCusStruct,string phone) { // } }
注意扩展方法的第一个参数是要扩展的类型,它放在this关键字的后面。这告诉编译器,这个方法是PhoneCusStruct类型的一部分。在这个例子中,PhoneCusStruct是要扩展的类型。在扩展方法中,可以访问所扩展类型的所有公有方法和属性。
调用:
PhoneCusStruct p =new PhoneCusStruct(); p.Add();//即使方法是静态方法,也需要使用实例方法的语法。
如果扩展方法与类中的某个方法同名,就不会调用扩展方法。类中已有的任何实例方法优先。