C#学习笔记(二)——类、委托、接口

静态成员

所谓静态成员,就是与实例无关、只与类有关的字段、方法、或者属性。静态成员的存在不依赖于实例的存在,即使没有任何该类的实例,静态成员也会存在与堆上。如果静态成员有初始化字段,那么会在该类的任何静态成员之前初始化该字。

静态成员的访问方式

静态成员可以使用实例.成员的方式来访问,也可以直接使用类.成员的方式来访问,个人认为后一种更合理。

也可以使用using static结构来引用静态属性,这样或许可以少写一些代码。

区分常量与静态量

常量没有自己的储存位置,在编译时就被编译器替换为字面量,可以理解为与C语言的#deinfe宏定义类似,而静态量在内存中有特定的存储位置,可以被修改。常量对类的每个实例都是可见的,而静态量可以通过改变访问修饰符的办法来改变对其他类的可见性。

再对比readonly

const字段只能在声明语句中被初始化,而readonly可以在声明语句中被初始化,也可以在构造函数中被初始化。readonly变量的其他特点与静态量都类似。

构造函数

构造函数与方法类似,但是有以下两点不同:

  • 构造函数的名称必须与类名系统;
  • 构造函数不能有返回值。

一个类可以有多个构造函数,也就是说构造函数可以被重载。如果没有为类定义构造函数,那么编译器会为类提供一个隐式的构造函数,这个构造函数无参数,方法体为空。如果你已经定义了构造函数,那么编译器不再提供默认构造函数。有时在继承一个类时编译器会提示基类没有默认构造函数,就与此有关。

静态构造函数

静态函数是用于初始化类的静态字段的,其会在任何静态成员被引用或创建类的任何实例之前被调用。一个类只能有一个静态构造函数,且不能有访问修饰符。

对象初始化语句

对象初始化语句的语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public 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};
}
}

初始化发生在构造方法执行之后,所以上文第12行的语句会吧对象中的X改为5,Y改为6。

类和继承

类和继承方面的内容在之前的博客中写过了,这里不再赘述。比较需要注意的就是C#中的函数都为实函数,如果希望其被子类的函数覆盖,就得为其添加virtual关键字并为子类的方法添加override关键字。在C#中屏蔽和覆盖的概念是不同的

扩展方法

当我们使用第三方库时会希望能够在第三方类中添加新的方法便于编程,但是如果第三方库的代码无法修改,且我们希望修改的第三方类是密封的(我们无法继承自该基类),就可以使用扩展方法,在第三方类中实现一个额外的方法,语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace ExtensionMethods
{
sealed class MyData
{
private double D1,D2,D3;
public Mydata(double d1,double d2,double d2)
{
//...
}

public double Sum(){ return D1+D2+D3};
}

static class ExtendMyData
{
public static double Average(this MyData md)
{
return md.Sum()/3;
}
}

class Program
{
static void Main()
{
MyData md = new MyData(3,4,5);
Console.WriteLine("Average: {0}",md.Average());
}
}
}

通过上面的扩展方法,我们可以直接把Average方法像调用MyData类的成员一样调用,既不需要继承,也不需要修改源代码,十分地神奇。需要注意的是新建的类和方法都必须是静态的,且方法的参数需要添加this关键字。

运算符

之前的文章提过运算符,但是并没有细说运算符重载,这里再大致介绍一下其语法。运算符重载只能用于类和结构,且根据我们重载的运算符是一元运算符还是二元运算符,相关的重载方法也应该具有对应的参数个数:

1
2
public static LimitedInt operator -(LimitedInt x);// 重载的是负数运算符而不是减法
public static LimitedInt operator +(LimitedInt x, double y);

可重载的一元运算符:+ - ! ~ ++ -- true false

可重载的二元运算符:+ - * / % & | ^ >> << == != > < >= <=

重载运算符只能定义一个新的操作结果(语义),而不能改变运算符的语法、结合性等语法特点。

用户定义的类型转换

类型转换实际上也是运算符的一种,可以分为隐式类型转换和显式类型转换。隐式类型转换是编译器在发现类型不对等时自动进行类型转换,而显示类型转换需要使用括号来表明要转换的对象。

语法如下:

1
2
3
4
public static implicit operator TargetType(SourceType param)
{
return ObjectOfTargetType;
}

上面展示的是隐式的类型转换对应的代码,如果希望定义一个显式类型转换,就把代码中的implicit改成explicit即可。两种类型转换其实十分相似,只是我们希望类型转换的场合不同。隐式转换的场合更加广泛,而显示转换只在必要的时候进行。

结构

结构看上去像是从C/C++搬过来的东西,结构和类十分类似,但不同的是结构是值类型,而且结构是密封的,不能被继承。虽然说结构的定义和C语言很类似,但是创建结构的语法却又和类类似,使用的是new语句。有new语句,就肯定少不了构造函数。我们可以为结构添加构造函数,但是需要注意的是,不论我们是否为其添加构造函数,结构都存在一个默认构造函数,这是与类不同的另外一个地方。

默认构造函数将值类型设置为默认值,引用类型设置成null。

也可以像C语言中一样不使用new来创建结构的实例,但是这样有一些限制:

  • 在显示设置数据成员的值之后,才能使用它们的值;
  • 在对所有数据成员赋值之后才能调用结构的函数成员。

总之,这么干听起来就很危险,不建议使用。

1
2
3
4
5
6
7
static void Main()
{
Simple s1;
Simple s2 = new Simple();
Console.WriteLine("{0}",s1.X); // 这么做会编译错误
Console.WriteLine("{0}",s2.X); // Fine
}

委托

什么是委托?《C#图解教程》中有一句十分浅显易懂的话:

如果你有C背景,理解委托最快的方法就是把它看成一个类型安全的、面向对象的C函数指针。

委托的相关语法

1
delegate void MyDel(int x)

delegate是关键词,后面跟着的方法的签名,但是MyDel并不是真正的方法名字,而是我们创建的委托名字。可以理解为,我们为所有的相同签名的方法都创建了一个模板(委托类型),用这个模板可以创建很多委托,每个委托中可以容纳很多的方法。

1
MyDel delVar;

上面这个语句用先前创建好的委托类型创建了一个委托,两者的关系就好像是C语言的结构体声明和结构体变量声明。

为委托的赋值的方法有两种,其中第二种为快捷语法,赋值的前提是方法的签名要和委托匹配。

1
2
delVar = new MyDel(myInstObj.Mym1);
delVar = MyInstObj.Mym1;

一个委托可以容纳多个方法,把它看成一个承载多种方法的容器是没有问题的。C#的委托之间实现了+运算,并且委托可以通过 += -=y运算符来添加和删除方法。

1
2
3
4
5
MyDel delA = MyInstObj.Mym1;
MyDel delB = SClass.OtherM1;
MyDel delC = delA + delB;
delVar += SCl.m3;
delVar += X.Act;

结果上面的代码, 变量delC中包含两个方法,且delAdelB所包含的方法并不会改变,这里可以看出,委托所包含的实际上是方法的引用。同理,+=操作也可以为委托添加方法,可以重复添加多个方法,而-=操作会从尾部开始搜索,找到匹配的方法便删除,试图删除委托中不存在的方法将无效。调用空的委托会报异常。

下面的代码示范了两种调用委托的方式:

1
2
delVar(55);
delVar.Invoke(65);

似乎第一种更简洁,但是第二种的代码可读性更高,因为Invoke这个方法很清晰地展示了delVar是一个委托而不是方法。

委托的花式用法

带返回值的委托

委托在声明时可以带返回值,但是一个委托的调用列表中如果有多个方法,那么只会返回最后一个方法的返回值。

带有引用参数的委托

参考下面的代码:

1
delegate void MyDel(ref int x);

当这个委托中有多个方法时并且被调用时,第一个方法会接受我们传递的实参,然后这个实参在经过第一个方法之后(可能)会被修改,修改后的参数值会被传递给下一个参数,以此类推。所以如果想要链式地修改一个值的话可以采用委托这个工具。

匿名方法

当为委托添加方法或者初始化时,等式右边的方法可能只在当前使用,其他地方根本用不上(类似Lambda表达式),C#提供了匿名方法这个语法糖来减少代码量。

1
2
3
4
5
6
7
8
9
delegate int OtherDel(int InParam);

static void Main()
{
OtherDel del = delegate(int x)
{
return x+20;
}
}

这么做,这个方法没有名字,且只被使用一次,大大减小了代码量。

Lambda表达式

实际上,C# 3.0又引入了Lambda表达式,使得代码量进一步缩减,上面的代码可以缩减为:

1
2
3
(int x) =>{return x+20};
(x) => {return x+20};
x => x+20;

当然,把Lambda表达式和委托结合的前提是Lambda表达式中的参数必须在数量、类型和位置上与委托匹配。


C#学习笔记(二)——类、委托、接口
http://zhouhf.top/2023/06/03/C-学习笔记(二)——类、委托、接口/
作者
周洪锋
发布于
2023年6月3日
许可协议