类和结构
类和结构都是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法。
类定义了类的每个对象(称为实例)可以包含什么数据和功能。还可以定义处理在这些字段中存储的数据的功能。
类
类中的数据和函数称为类的成员,除了这些成员外,类还可以包含嵌套的类型(如其它类),成员的可访问性可以是:
- public
- protected
- internalprotected
- private
- internal
数据成员
数据成员是包含类的数据:字段、常量和事件的成员,数据成员可以是静态数据,类成员总是实例成员,除非用 static
进行显式声明。
函数成员
函数成员提供了操作类中数据的某些功能,包括方法、属性、构造函数和终结器、运算符以及索引器
- 方法是与某个类相关的函数,与数据成员一样,函数成员默认为实例成员,使用
static
修饰符可以把方法定义为静态方法 - 属性是可以从客户端访问的函数组,其访问方式与访问类的公共字段类似,
C#
为读写类中的属性提供了专用语法,所以不必使用名称中嵌有Get
或set
的方法 - 构造函数是在实例化对象时自动调用的特殊函数,必须与所属的类同名,且不能有返回类型,构造函数用于初始化字段的值
- 终结器类似于构造函数,在
CLR
检测到不再需要某个对象时调用它,它们名称与类相同,但前面有一个~
- 运算符执行的最简单的操作就是加法和减法。 在两个整数相加时,严格地说,就是对整数使用
+
运算符,C#允许指定把已有的运算符应用于自己的类(运算符重载) - 索引器允许对象以数组或集合的方式进行索引
方法
方法声明
在 C#中 ,方法的定义包括任意方法修饰符(如方法的可访问性)、返回值的类型,然后依次是方法名和输入参数的列表,和方法体。每个参数都包括参数的类型名和在方法体中的引用名称。 如果方法有返回值,return
语句就必须与返回值一起使用。
[modifiers] return_type MethodName([parameters])
{
// Method body
}
class Test
{
/// <summary>
/// 实例方法
/// </summary>
public void Show()
{
Console.WriteLine("Test下实例方法");
}
/// <summary>
/// 静态方法属于类本身
/// </summary>
public static void Show2()
{
Console.WriteLine("Test下静态方法");
}
}
方法调用
在实例化类得到类的对象后,即可通过 “对象.方法名称” 进行方法的调用。
// 实例方法调用
Test test = new Test();
test.Show();
// 静态方法调用
Test.Show2();
注意:使用
static
修饰的方法属于类的本身,无法使用类的实例化对象进行调用,使用类名.方法名即可
表达式方法体
如果方法的实现只有一条语句,C#为方法定义提供了一个简化的语法:表达式体方法,使用 =>
区分操作符左边的声明和操作符右边的实现代码。
public void PrintName() => Console.WriteLine(_name);
命名参数
参数一般需要按定义的顺序传送给方法,命名参数允许按任意顺序传递。
class Program
{
static void Main(string[] args)
{
//下面两种调用方式结果是一样的
Print("hello", "world");
Print(b: "world", a: "hello");
Console.ReadKey();
}
static void Print(string a, string b)
{
Console.WriteLine("{0},{1}", a, b);
}
}
可选参数
参数也可以是可选的,必须为可选参数提供默认值,可选参数还必须是方法定义的最后一个参数。
static void Print(string a, string b = "world")
{
Console.WriteLine("{0},{1}", a, b);
}
Print("hello");//输出hello,world
Print("hello","wang");//输出hello,wang
个数可变的参数
使用 params[]
数组的方式可以定义数量可变的参数,如果 params[]
关键字与方法签名定义的多个参数一起使用,则只能使用一次,而且必须是最后一个参数。
方法重载
方法名相同,但参数的个数或类型不同。
static void Print(string a)
{
Console.WriteLine("{0}", a);
}
static void Print(string a, string b = "world")
{
Console.WriteLine("{0},{1}", a, b);
}
注意:
- 两个方法不能仅在返回类型上有区别
- 两个方法不能仅根据参数是声明为
ref
还是out
来区分
属性
它是一个方法或一对方法,在客户端代码看来,它(们)是一个字段。
属性定义
public string SomeProperty
{
get
{
return "This is the property value";
}
set
{
//type string
}
}
只读和只写属性
在属性定义中省略 set
访问器,就可以创建只读属性;同样,在属性定义中省略 get
访问器,就可以创建只写属性。
class Program
{
private string age;
private string name;
//只读属性
public string Age
{
get
{
return age;
}
}
//只写属性
public string Name
{
set
{
value= name;
}
}
}
属性的访问修饰符
C#允许给属性的 get
和 set
访问器设置不同的访问修饰符,所以属性可以有公有的 get
访问器和私有或受保护的 set
访问器。有助于控制属性的设置方式。
自动实现的属性
如果属性的 get
和 set
访问器中没有任何逻辑,就可以使用自动实现的属性,使用自动实现的属性,就不能在属性设置中验证属性的有效性。
class Program
{
public string Age { get; set; }
public string Name { get; set; }
static void Main(string[] args)
{
Console.ReadKey();
}
}
构造函数
声明基本构造函数的语法就是声明一个与包含的类同名的方法,但该方法没有返回类型。
构造函数声明
class Person
{
Person()
{
}
}
注意:没有必要给类显式提供构造函数,原因在于:如果没有在类中没有提供任何构造函数,编译器会在后台创建一个默认的无参构造函数用来把所有的成员字段初始化为标准的默认值。
构造函数重载
构造函数重载遵循与其他方法相同的规则,就是说允许为构造函数提供任意多的重载。
class Program
{
static void Main(string[] args)
{
//调用无参构造函数实例化对象
Person person1 = new Person();
//调用带参数的构造函数实例化对象
Person person2 = new Person(3);
//因为带name参数的构造函数是private的,所以这里无法实例化
//Person person3 = new Person(20, "wang");
Console.ReadKey();
}
}
class Person
{
private int age;
private string name;
public Person()
{
}
public Person(int age)
{
//使用this关键字区分成员字段和同名参数
this.age = age;
}
private Person(int age, string name)
{
this.age = age;
this.name = name;
}
}
class Person
{
private int age;
private string name;
private Person(int age, string name)
{
this.age = age;
this.name = name;
}
}
这个例子没有为 Person
类定义任何公有的或受保护的构造函数。这就使 Person
不能使用 new
运算符在外部代码中实例化(但可以在 Person
中编写一个公有静态属性或方法,以实例化该类)。 这在下面两种情况下是有用的
- 类仅用作某些静态成员或属性的容器,因此永远不会实例化它
- 希望类仅通过调用某个静态成员函数来实例化(单例模式)
注意:如果提供了带参数的构造函数,编译器就不会隐式的自动创建默认的构造函数
构造函数初始化器
有时,在一个类中有几个构造函数,以容纳某些可选参数,这些构造函数包含一些共同的代码。需要做到从构造函数中调用其他构造函数时可以使用构造函数初始化器。
class Program
{
static void Main(string[] args)
{
Person person1 = new Person(20);
Person person2 = new Person(20, "li");
Console.WriteLine("person1:age={0},name={1}", person1.age, person1.name);
Console.WriteLine("person2:age={0},name={1}", person2.age, person2.name);
Console.ReadKey();
}
}
class Person
{
public int age;
public string name;
public Person(int age)
{
this.age = age;
this.name = "wang";
}
public Person(int age, string name)
{
this.age = age;
this.name = name;
}
}
上面的例子是一个简单的构造函数重载,然后通过调用不同的构造函数实例化对象,Person
类的两个构造函数初始化了相同的字段 age
,显然最好把所有的代码放在一个地方,C#中使用构造函数初始化器,可以实现此目的:
class Program
{
static void Main(string[] args)
{
Person person1 = new Person(20);
Person person2 = new Person(20, "li");
Console.WriteLine("person1:age={0},name={1}", person1.age, person1.name);
Console.WriteLine("person2:age={0},name={1}", person2.age, person2.name);
Console.ReadKey();
}
}
class Person
{
public int age;
public string name;
public Person(int age)
{
this.age = age;
this.name = "wang";
}
public Person(int age, string name) : this(age)
{
this.age = age;
this.name = name;
}
}
这里 this
关键字仅调用参数最匹配的那个构造函数。
注意,构造函数初始化器在构造函数的函数体之前执行。C#构造函数初始化器可以包含对同一个类的另一个构造函数的调用(使用前面介绍的语法),也可以包含对直接基类的构造函数的调用(使用相同的语法,但应使用
base
关键字代替this
。初始化器中不能有多个调用
静态类
如果类只包含静态的方法和属性,该类就是静态的。静态类在功能上与使用私有静态函数创建的类相同,不能创建静态类的实例。
静态构造函数
静态构造函数只执行一次,而前面的构造函数是实例构造函数,只要创建类的对象,就会被执行。编写静态构造函数的一个原因是,类有一些静态字段或属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性。
注意:
.Net
运行库没有确保什么时候执行静态构造函数,所以不应把要求在某个特定时刻(例如,加载程序集时)执行的代码放在静态构造函数中。也不能预计不同类的静态构造函数按照什么顺序执行。但是,可以确保静态构造函数最多运行一次,即在代码引用类之前调用它
在C#中,通常在第一次调用类的任何成员之前执行静态构造函数。静态构造函数没有访问修饰符,其他C#代码从来不调用它,但在加载类时,总是由.NET运行库调用它,所以像public
或 private
这样的访问修饰符就没有任何意义。出于同样原因,静态构造函数不能带任何参数,一个类也只能有一个静态构造函数。很显然,静态构造函数只能访问类的静态成员,不能访问类的实例成员。
无参数的实例构造函数与静态构造函数可以在同一个类中同时定义。尽管参数列表相同,但这并不矛盾,因为在加载类时执行静态构造函数,而在创建实例时执行实例构造函数,所以何时执行哪个构造函数不会有冲突。如果多个类都有静态构造函数,先执行哪个静态构造函数就不确定。此时静态构造函数中的代码不应依赖于其他静态构造函数的执行情况。 另一方面,如果任何静态字段有默认值,就在调用静态构造函数之前指定它们。
结构
结构是值类型
结构是会影响性能的值类型,但根据使用结构的方式,这种影响可能是正面的,也可能是负面的。 正面的影响是为结构分配内存时,速度非常快,因为它们将内联或者保存在栈中。 在结构超出了作用域被删除时,速度也很快。负面影响是,只要把结构作为参数来传递或者把一个结构赋予另一个结构(如A-B,其 中A和 B是结构),结构的所有内容就被复制,而对于类,则只复制引用。 这样就会有性能损失,根据结构的大小,性能损失也不同。
注意:结构主要用于小的数据结构。但当把结构作为参数传递给方法时,应把它作为
ref
参数传递,以避免性能损失,此时只传递了结构在内存中的地址,这样传递速度就与在类中的传递速度一样快了。但如果这样做,就必须注意被调用的方法可以改变结构的值
结构不支持继承
结构(和C#中的其他类型一样)最终派生于类 System.Object
。因此结构也可以访问 System.Object
的方法。在结构中,甚至可以重写 System.Object
中的方法(如重写Tostring)方法。 结构的继承链是:每个结构派生自 System.ValueType
类 ,System.ValueType
类又派生自 System.Object
。 ValueType
并没有给 Object
添加任何新成员,但提供了一些更适合结构的实现方式。
注意:不能为结构提供其他基类,每个结构都派生自
ValueType
结构的构造函数
为结构定义构造函数的方式与为类定义构造函数的方式相同,但不允许定义无参数的构造函数。.Net运行库禁止在C#结构内定义无参构造函数。
类和结构的区别
结构与类的区别是它们在内存中的存储方式、访问方式(类是存储在堆上的引用类型,而结构是存储在栈上的值类型)和它们的一些特征(如结构不支持继承。较小的数据类型使用结构可提高性能。但在语法上,结构与类非常相似,主要的区别是使用关键字 struct
代替 class
来声明结构。对于类和结构,都使用关键字 new
来声明实例创建对象并对其进行初始化。
匿名类型
匿名类型只是一个继承自 Object
且没有名称的类。该类的定义从初始化器中推断,类似于隐式类型化的变量
var person = new { Name = "wang", Age = 22 };