要定义新的类或类型,首先要声明它,然后定义其方法和字段。完整语法如下:
[性质] [访问修饰符] class 标识符 [:基类]
{类主体}
在类主体中,定义类的所有对象的属性和行为,不能直接向新创建的类赋值,而是必须先创建一个该类型的对象,然后再对其实例的字段赋值。
- 类成员的类型
数据成员(保存数据) | 函数成员(执行代码) |
---|---|
字段,常量 | 方法,属性,运算符,索引,事件,构造函数,析构函数 |
类成员可以关联到类的每个实例,也可以关联到类的整体,即所有类的实例。类的每个实例都有自己的各个类成员的副本,这些成员即实例成员。改变一个实例字段的值不会影响任何其他实例中成员的值。
namespace InstanceMember
{
class D
{
public int Mem1;
}
class Program
{
static void Main()
{
D d1 = new D();
D d2 = new D();
d1.Mem1 = 10;
d2.Mem1 = 20;
Console.WriteLine("d1={0},d2={1}", d1.Mem1, d2.Mem1);
}
}
}
- 静态字段
除了实例字段,类还可以有静态字段,被类的所有实例共享,所有实例都要访问同一内存位置,如果该内存位置的值被一个实例改变了,这种改变对所有的实例都可见。
namespace InstanceMember
{
class D
{
public int Mem1;
public static int Mem2 = 6;
public void ChangeMem2(int value)
{
Mem2 = value; //类内部,像访问实例字段一样访问它
}
}
class Program
{
static void Main()
{
D d1 = new D();
D d2 = new D();
d1.Mem1 = 10;
d2.Mem1 = 20;
Console.WriteLine("d1={0},d2={1}", d1.Mem1, d2.Mem1);
Console.WriteLine("before modification, Mem2 is {0}", D.Mem2);
d1.ChangeMem2(12);
Console.WriteLine("after modification,Mem2 is {0}", D.Mem2); //类外部访问static 字段
D.Mem2 = 24;
Console.WriteLine("modification occurs in the outside of class D");
}
}
}
需要注意的是:静态成员必须使用类名,用点运算符来从类的外部访问,静态字段即使没有类的实例也存在。如果静态字段有初始化语句,那么会在使用该类的任何静态成员之前初始化该字段,但没必要在程序执行的开始就初始化。
- 静态函数成员
如同静态字段,静态函数成员独立于任何类实例。即使没有类的实例,仍然可以调用静态方法,注意,静态函数成员不能发访问实例成员,然而他们可以访问其他静态成员,静态函数可以被实例成员访问,调用.
如以下代码,静态方法不能访问实例字段:
如以下代码,静态方法不能调用实例函数.
如下代码,实例函数可以调用静态函数,可以访问静态字段.
namespace PopertyExample
{
class D
{
static int Value1 = 3;
public static void Test1()
{
Console.WriteLine("This is static methtod Test1");
}
public void Test2()
{
Console.WriteLine("Try to call Test1:");
Test1();
Console.WriteLine("value1 is {0}", Value1);
}
//public static void Test3()
//{
// Console.WriteLine("Try to call Test2");
// Test2();
//}
}
class Program
{
static void Main()
{
D d = new D();
d.Test2();
}
}
}
- 成员常量
成员常量类似于本地常量,只是它们被声明在类声明中而不是方法内,在声明时必须赋值,且该值在编译时必须是可计算的,而且通常是一个预定义简单类型或由它们组成的表达式。
- 常量与静态量
成员常量表现得像静态值,它们对类的每个实例都是“可见的”,而且没有类的实例也可以使用。与真正的静态量不同,常量没有自己的存储位置,而是在编译时被编译器替换,常量不能被后续赋值,修改。
如上图,试图对常量赋值,将会抛出异常。
如上图,试图用实例来访问静态量和常量,均会抛出异常,即从外部访问静态量和常量,必须使用类名.
另外需要注意的是:虽然常量成员表现的像一个静态值,但不能将常量声明为static,如下面代码是错误的:
static const double PI=3.14;//错误,不能将常量声明为static.
- 属性
示例:
namespace PopertyExample
{
class D
{
private int TheRealValue;
public int MyValue
{
set
{
TheRealValue = value;
}
get
{
return TheRealValue;
}
}
public void ShowValue()
{
Console.WriteLine("The real value is {0}", TheRealValue);
}
}
class Program
{
static void Main()
{
D d = new D();
Console.Write("Initially, the value is");
d.ShowValue();
d.MyValue = 10;
Console.Write("After assigned again, the value is:");
d.ShowValue();
}
}
}
通过上述实例,可以看到:
(1)属性本身没有任何存储,取而代之的是,访问器决定如何处理发进来的数据,以及什么数据被发出去,在这种情况下,属性使用一个名称为TheRealValue的字段来存储.
(2)set
访问器接受它的输入参数value,并把它的值赋给字段TheRealValue
.
(3)get
访问器只是返回字段TheRealValue的值.
(4)属性会根据是写入还是读取,来隐式地调用适当的访问器,不能显式地调用访问器.
(5)与属性关联的字段成为关联字段(或者后备字段),如上例中的TheRealValue
,一般声明为private
. 属性和它们的后备字段的命名有几种约定.一种是两个名称使用相同的内容,但字段使用Camel大小写,属性使用Pascal大小写,另一种约定是属性使用Pascal大小写,字段以相同标识符的Camel大小写版本,并以下划线开始.
(6)属性访问器并不仅仅局限于对关联的后备字段进行传入传出数据,访问器get和set能执行任何计算,或者不执行任何计算,唯一必须的行为是get访问器要返回一个属性类型的值.
namespace PopertyExample
{
class D
{
private int TheRealValue;
public int MyValue
{
set
{
Console.WriteLine("set the bigger one of value and 100");
TheRealValue = value>100? value: 100;
}
get
{
return TheRealValue;
}
}
public void ShowValue()
{
Console.WriteLine("The real value is {0}", TheRealValue);
}
}
class Program
{
static void Main()
{
D d = new D();
Console.Write("Initially, the value is");
d.ShowValue();
d.MyValue = 10;
Console.Write("After assigned again, the value is:");
d.ShowValue();
d.MyValue = 150;
Console.Write("After assigned again, the value is:");
d.ShowValue();
}
}
}
(7)只读和只写属性:要想不定义属性的某个访问器,可以忽略该访问器的声明,但两个访问器中至少有一个必须被定义,否则编译器会产生一条错误信息.
例:
namespace PopertyExample
{
class RightTriangle
{
public double A = 3;
public double B = 4;
public double Hypotenuse
{
get;
{
return Math.Sqrt((A * A) + (B * B));
}
}
}
class Program
{
static void Main()
{
RightTriangle c = new RightTriangle();
Console.WriteLine("Hypotenuse is :{0}", c.Hypotenuse);
}
}
}
(8)按照推荐的编码实践,属性比公共字段更好,因为:
(a)属性是函数成员,而不是数据成员,允许处理输入输出,而公共字段不可以.
(b)属性可以只读或只写,而字段不行.
(c)编译后的变量和编译后的属性语义不同.
(9)自动实现属性。C#提供了自动实现属性,允许只声明属性而不声明后备字段。编译器会子哦对那个创建隐藏的后备字段,并且自动挂接到get和set访问器上。要点为:
(a)不声明后备字段.
(b)不能提供访问器的方法体,它们必须被简单的声明为分号.
(c)除非通过访问器,否则不能访问后备字段,因为不能用其他方法访问它,所以实现只读和只写属性无意义,因此建议同时提供读写访问器.
namespace PopertyExample
{
class C1
{
public int MyValue
{
set;get;
}
}
class Program
{
static void Main()
{
C1 c = new C1();
Console.WriteLine("Myvalue:{0}", c.MyValue);
c.MyValue = 20;
Console.WriteLine("Myvalue:{0}", c.MyValue);
}
}
}
只提供get或set访问访问器:
(10)静态属性.属性也可以被声明为static
,静态属性的访问器与所有静态成员一样,具有以下特点:
(a)不能访问类的实例成员,即实例字段与函数,但它们可以被实例成员访问到.
(b)不管类是否有实例,它们都是存在的.
(c)从类的外部访问时,必须使用类名引用,而不是实例名.
namespace PopertyExample
{
class Trival
{
public static int MyValue { get; set; }
public void PrintValue()
{
Console.WriteLine("value from inside:{0}", MyValue); // 从类的内部访问
}
}
class Program
{
static void Main()
{
Console.WriteLine("Init value:{0}", Trival.MyValue);//从类的外部访问
Trival.MyValue = 10;
Console.WriteLine("New value:{0}", Trival.MyValue);
Trival t = new Trival();
t.PrintValue();
}
}
}
(11)属性不具备重载的特征。
如下代码:
- 实例构造函数
实例构造函数,在创建类的每个新实例时执行,用于初始化实例的状态,如果希望能从类的外部创建类的实例,需要将构造函数声明为public
。
需要注意的是构造函数的名称和类名相同,且不能有返回值
(1)带参数的构造函数,可以被重载。
namespace ConstructorExample
{
class Class1
{
int ID;
string Name;
public Class1() { ID = 28;Name = "JohnYang"; }
public Class1(int Val) { ID = Val; Name = "JohnYang"; }
public Class1(string name) { Name = name; }
public void SoundOff()
{
Console.WriteLine("Name:{0},ID:{1}", Name, ID);
}
}
class Program
{
static void Main()
{
Class1 a = new Class1(),
b = new Class1(7),
c = new Class1("Bill");
a.SoundOff();
b.SoundOff();
c.SoundOff();
}
}
}
(2)默认构造函数。如果在类的声明中没有显式地提供实例构造函数,那么编译器会提供一个隐式的默认构造函数,它具备如下特征:
(a)没有参数(b)方法体为空。如果为类声明了任何构造函数,那么编译器将不会为该类定义默认构造函数。
- 静态构造函数
构造函数也可以声明为static
,实例构造函数初始化类的每个实例,而static
构造函数初始化类级别的项。通常构造函数初始化类的静态字段。在引用任何静态成员之前,在创建任何实例之前,需要注意的是静态构造函数中使用static
关键字,类只能有一个静态构造函数,而且不能带参数,故不存在函数重构,静态构造函数不能有访问修饰符。
类既可以有静态构造函数也可以有实例构造函数,如同静态方法,静态构造函数不能访问所在类的实例成员,因此也不能使用this
访问器,不能从程序中显式的调用静态构造函数,系统会在(a)类的任何实例被创建之前(b)类的任何静态成员被引用之前;这两种情况下自动调用静态构造函数。
namespace ConstructorExample
{
class RandomNumberClass
{
private static Random RandomKey; //私有静态字段
static RandomNumberClass() //静态构造函数
{
RandomKey = new Random(); //初始化RandomKey
}
public int GetRandomNumber()
{
return RandomKey.Next();
}
}
class Program
{
static void Main()
{
RandomNumberClass a = new RandomNumberClass(),
b = new RandomNumberClass();
Console.WriteLine("Next Random:{0}",a.GetRandomNumber());
Console.WriteLine("Next Random{0}", b.GetRandomNumber());
}
}
}
在上例中,在任何类实例创建前,系统自动调用RandomNumberClass
静态构造函数,为私有静态字段RandomKey
赋值,最终在类内部,实例函数成员GetRandomNumber
通过访问静态字段RandomKey
,得到随机数。
- 对象初始化语句
对象创建表达式由关键字new
后面跟着一个类构造函数及其参数列表组成。对象初始化语句扩展了创建语法,在表达式尾部放置了一组成员初始化语句。这允许在创建新的对象实例时,设置字段和属性的值。
该语法有两种形式,一种形式包括构造函数的参数列表,另一种不包括,注意第一种形式甚至不适用括号起参数列表的圆括号。
new TypeName {FieldOrProp=IniExpr, FieldOrProp=InitExpr,...} //第一种形式
new TypeName(ArgList) {FieldOrProp=InitExpr,FieldOrProp=InitExpr,...} //第二种形式
需要注意的是:
(1)创建对象的代码必须能够访问要初始化的字段和属性,比如字段必须是public
的。
(2)初始化发生在构造函数执行之后,因此在构造函数种设置的值可能会在之后对象初始化中重置为不同的值。
namespace ConstructorExample
{
class Point
{
public int X = 1;
public int Y = 2;
}
class Program
{
static void Main()
{
Point pt1 = new Point();
Point pt2 = new Point { X = 5, Y = 6 };
Console.WriteLine("pt1:{0},{1}", pt1.X, pt1.Y);
Console.WriteLine("pt2:{0},{1}", pt2.X, pt2.Y);
}
}
}
- readonly 修饰符
字段可以用readonly
修饰符声明。其作用类似于将字段声明为const
,一旦值被设定就不能改变。不同的是:
(1)const字段只能在声明语句中初始化,而readonly字段除了可以在声明语句中初始化,也可以在类的任何构造函数中(如果是static字段,必须是静态构造函数)中完成赋值。
(2)const字段必须在编译时决定,而readonly字段的值可以在运行时决定,这种增加的自由性允许在不同的环基或不同的构造函数中设置不同的值。
(3)和const不同,const行为总是静态的(类似于静态成员,外部访问只能通过类名访问),而对于readonly可以是实例字段,也可以是静态字段。
namespace ConstructorExample
{
class Shape
{
readonly double PI = 3.14;
public readonly int NumberOfSides;//未初始化
public Shape(double side1,double side2) //构造函数
{
NumberOfSides = 4;
}
public Shape(double side1,double side2,double side3) //构造函数
{
NumberOfSides = 3;
}
}
class Program
{
static void Main()
{
Shape sp = new Shape(3, 4);
Shape sp1 = new Shape(3, 2, 1.5);
Console.WriteLine("sp.NumberOfSides is {0} and sp1.NumberOfSides is {1}",
sp.NumberOfSides, sp1.NumberOfSides);
}
}
}
- this 关键字
this 关键字在类中使用,是对当前实例的引用。只能用在下列类成员的代码块中:
(1)实例构造函数
(2)实例方法
(3)属性和索引器的实例访问器
很明显,静态成员不是实例的一部分,索引不能再任何静态函数成员的代码中使用this关键字,更适当地讲,this用于以下目的:
(1)用于区分类成员和本地变量和参数
(2)作为调用方法的实参。
namespace ThisExample
{
class MyClass
{
int Var1 = 10;
public int ReturnMaxSum(int Var1)
{
return Var1 > this.Var1 ? Var1 : this.Var1;
}
}
class Program
{
static void Main()
{
MyClass mc = new MyClass();
Console.WriteLine("Max:{0}", mc.ReturnMaxSum(30));
Console.WriteLine("Max:{0}", mc.ReturnMaxSum(5));
}
}
}
上述代码仅仅作为一个例子,方法比较参数和字段的值并返回较大的值。唯一的问题是字段和形参的名称相同,都是Va1l
。在方法内使用this 关键字引用字段,以区分这两个名称。注意,不推荐参数和类型的字段使用相同的名称。
- 索引器
索引器是一组get和set访问器,与属性类似,索引器也不用来分配内存来存储,与属性类似,都用来访问其他类型数据成员,它们与这些成员关联,并为它们提供获取和设置访问,两者都可以只有一个访问器。两者都不能显式地调用get和set访问器。
不同的是属性通常表示单独的数据成员,其有标识符,索引器表示多个数据成员,且无标识符,索引器总是实例成员,不能被声明为static。
(1)声明索引器
ReturnType this [Type Param1,...] // 分别是返回类型 this关键字 [索引参数]
{
get
{
....
}
set
{
....
}
}
当索引器被用于赋值时,set访问器被调用,并接受两项数据:
(a)一个隐式参数,名为value,value持有要保存的数据
(b)一个或更多索引参数,表示数据应保存到哪里。
set访问器中的代码必须检查索引参数,以确定数据应存放到哪里,然后存放它。
当使用索引器获取值时,可以通过一个或多个索引参数调用get访问器。get访问器方法体内的代码必须检查索引参数,确定它表示的是哪个字段,并返回该字段的值。
namespace IndexExample
{
class Employee
{
public string LastName;
public string FirstName;
public string CityOfBirth;
public string this [int index]
{
set
{
Console.WriteLine("setting value {0} to index {1}", value, index);
switch (index)
{
case 0:LastName = value;
break;
case 1:FirstName = value;
break;
case 2:CityOfBirth = value;
break;
default:
throw new ArgumentOutOfRangeException("index");
}
}
get
{
Console.WriteLine("getting index {0}", index);
switch (index)
{
case 0:return LastName;
case 1: return FirstName;
case 2: return CityOfBirth;
default:
throw new ArgumentOutOfRangeException("index");
}
}
}
}
class Program
{
static void Main()
{
Employee emp = new Employee();
emp[0] = "Yang";
emp[1] = "John";
emp[2] = "LZ";
Console.WriteLine("emp[0] is {0}, emp[1] is {1}, emp[2] is {2}", emp[0], emp[1], emp[2]);
}
}
}
(2)索引器重载
只要索引器的参数列表不同,类就可以有任意多个索引器。
namespace IndexExample
{
class Employee
{
public string LastName;
public string FirstName;
public string CityOfBirth;
public string this[int index]
{
set
{
//Console.WriteLine("setting value {0} to index {1}", value, index);
switch (index)
{
case 0:
LastName = value;
break;
case 1:
FirstName = value;
break;
case 2:
CityOfBirth = value;
break;
default:
throw new ArgumentOutOfRangeException("index");
}
}
get
{
//Console.WriteLine("getting index {0}", index);
switch (index)
{
case 0: return LastName;
case 1: return FirstName;
case 2: return CityOfBirth;
default:
throw new ArgumentOutOfRangeException("index");
}
}
}
public string this[string index]
{
set
{
//Console.WriteLine("setting value {0} to index {1}", value, index);
switch (index)
{
case "0":
LastName = value;
break;
case "1":
FirstName = value;
break;
case "2":
CityOfBirth = value;
break;
default:
throw new ArgumentOutOfRangeException("index");
}
}
get
{
//Console.WriteLine("getting index {0}", index);
switch (index)
{
case "0": return LastName;
case "1": return FirstName;
case "2": return CityOfBirth;
default:
throw new ArgumentOutOfRangeException("index");
}
}
}
}
class Program
{
static void Main()
{
Employee emp = new Employee();
emp[0] = "Yang";
emp[1] = "John";
emp[2] = "LZ";
Console.WriteLine("emp[0] is {0}, emp[1] is {1}, emp[2] is {2}", emp[0], emp[1], emp[2]);
emp["0"] = "Yang";
emp["1"] = "John";
emp["2"] = "LZ";
Console.WriteLine("emp['0'] is {0}, emp['1'] is {1}, emp['2'] is {2}", emp[0], emp[1], emp[2]);
}
}
}
- 访问器的修饰符
属性和索引器这两种函数成员带set和get访问器,默认情况下,成员的两个访问器有和成员自身相同的访问级别。不过,也可为两个访问器分配不同的访问级别。
namespace AccessExample
{
class Person
{
public string Name { get; private set; }//不同级别的访问器,set只能在类内部设置,这里是在构造器中设置
public Person(string name)
{
Name = name;
}
}
class Program
{
static void Main()
{
Person p = new Person("JohnYang");
Console.WriteLine("Person's name is {0}", p.Name);
}
}
}
上例中,尽管可以从类的外部读取属性,但却只能在类的内部设置它。