<NET CLR via c# 第4版>笔记 第8章 方法
8.1 实例构造器和类(引用类型)
-
构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零 。没有被构造器显式重写的所有字段都保证获得 0 或 null 值。
-
构造器不能被继承。不能使用以下修饰符: virtual,new,override,sealed和abstract.
-
如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。
-
如果类的修饰符为 static(sealed 和 abstract),编译器不会生成默认构造器。(静态类在元数据中是抽象密封类)
-
为了使代码“可验证”,类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。
-
如果派生类的构造器没有显式调用一个基类构造器,C#编译器会自动生成对默认的基类构造器的调用。
-
极少数时候可以在不调用实例构造器的前提下创建类型的实例(如 MemberwiseClone 方法)。
-
不要在构造器中调用虚方法。 原因是假如被实例化的类型重写了虚方法,就会执行派生类型对虚方法的实现。但在这个时候,尚未完成对继承层次结构中的所有字段的初始化(被实例化的类型的构造器还没有运行呢)。所以,调用虚方法会导致无法预测的行为。
-
构造器中,先执行以“内联”方式对字段的初始化(C#编译器将这种语法转换成构造器方法中的代码来执行初始化),再调用基类的构造器,最后执行当前构造器中的代码。详见下面代码:
public class AType {
public AType()
{
Console.WriteLine(nameof(AType));
}
}
public class Ball
{
public Ball()
{
Console.WriteLine(nameof(Ball));
}
}
public class Basketball:Ball
{
//内联语法,最终会把atype的初始化放在构造函数中
AType atype = new AType();
public Basketball()
{
Console.WriteLine(nameof(Basketball));
}
}
public class Program
{
static void Main()
{
Basketball ball = new Basketball();
Console.ReadLine();
}
}
输出结果为: AType,Ball,Basketball
- 然后又做了下面的BT测试:
public class AType
{
public AType(string x)
{
Console.WriteLine(nameof(AType) + ":" + x);
}
}
public class Ball
{
//在基类中新增加个内联方式初始化的字段
private AType atype = new AType(nameof(Ball));
public Ball()
{
Console.WriteLine(nameof(Ball));
}
}
public class Basketball : Ball
{
//内联语法,最终会把atype的初始化放在构造函数中
private AType atype = new AType(nameof(Basketball));
public Basketball()
{
Console.WriteLine(nameof(Basketball));
}
}
public class Program
{
static void Main()
{
Basketball ball = new Basketball();
Console.ReadLine();
}
}
输出结果为:AType:Basketball, AType:Ball, Ball, Basketball
-
使用“内联”方式初始化字段,要注意代码的膨胀效应。如果类中有多个构造器,编译器会把“内联”初始化代码插到每一个构造器中,再插入基类构造器的调用,最后执行自己的代码。
-
基于上面一点,可考虑不在定义字段时初始化,而是创建单个构造器来执行这些公共的初始化。然后,让其他构造器利用 this 显式调用这个公共初始化构造器,如下面代码:
internal sealed class SomeType
{
//不要显式初始化下面的字段
private int m_x;
private string m_s;
private double m_d;
private byte m_b;
//该构造器将所有字段都设为默认值,
//其他所有构造器都显式调用该构造器
public SomeType()
{
m_x = 5;
m_s = "Hi there";
m_d = 3.14159;
m_b = 0xff;
}
//该构造器将所有的字段都设为默认值,然后修改m_x
public SomeType(int x) : this()
{
m_x = x;
}
//该构造器将所有的字段都设为默认值,然后修改m_s
public SomeType(string s) : this()
{
m_s = s;
}
//该构造器将所有的字段都设为默认值,然后修改m_x和m_s
public SomeType(int x, string s) : this()
{
m_x = x;
m_s = s;
}
}
8.2 实例构造器和结构(值类型)
-
C#编译器根本不会为值类型内联(嵌入)默认的无参构造器。结构也不能包含显式的无参数构造函数(编译器会报错)。
-
考虑到性能,CLR不会为包含在引用类型中的每个值类型字段都主动调用构造器。值类型的实例构造器只有显式调用才会执行。
-
值类型的任何构造器都必须初始化值类型的全部字段。(如未全部初始化,编译器会报错)
8.3 类型构造器
-
类型构造器(静态构造器) 可应用于接口(C#编译器不允许)、引用类型和值类型(但请永远不要在值类型中定义类型构造器,因为CLR有时不会调用值类型的静态类型构造器)。
-
类型构造器必须是 private 的,但不能显示标记,只能由编译器来标记为 private .
-
类型构造器必须无参。
-
CLR保证一个类型构造器在每个AppDomain中只执行一次,而且(这种执行)是线程安全的。 所以非常适合在类型构造器中初始化类型需要的任何单实例(Singleton)对象。
8.4 操作符重载方法
-
CLR规范要求操作符重载方法必须是 public 和 static 方法。
-
C#(以及其他许多语言)要求操作符重载方法至少有一个参数的类型与当前定义这个方法的类型相同。
public sealed class Complex
{
//重载“+”操作符
public static Complex operator +(Complex c1, Complex c2){...}
}
- 使用不支持操作符重载的编程语言时,语言应该允许你直接调用希望的 op_* 方法(例如 op_Addition)。反之,如果在C#中引用了不支持操作符重载的语言所写的类型,且类型中提供了一个 op_Addition 方法,这时依然不能使用+操作符来调用这个 op_Addition 方法,因为元数据中没有关联 specialname 标记。
8.5 转换操作符方法
-
CLR要求操作符重载方法必须是 public 和 static 方法。
-
C#(以及其他许多语言)要求参数类型和返回类型二者必有其一与定义转换方法的类型相同。
-
类型转换模板代码:
public sealed class Rational {
//由一个Int32构造一个Rational
public Rational(Int32 num) { ...}
//由一个Single构造一个Rational
public Rational(Single num) { ...}
//将一个Rational转换成一个Int32
public Int32 ToInt32() { ...}
//将一个Rational转换成一个Single
public Single ToSingle() { ...}
//由一个Int32隐式构造并返回一个Rational
public static implicit operator Rational(Int32 num) {
return new Rational(num);
}
//由一个Single隐式构造并返回一个Rational
public static implicit operator Rational(Single num) {
return new Rational(num);
}
//由一个Rational显示返回一个Int32
public static explicit operator Int32(Rational r) {
return r.ToInt32();
}
//由一个Rational显示返回一个Single
public static explicit operator Single(Rational r) {
return r.ToSingle();
}
}
//像前面那样为 Rational 类型定义了转换操作符之后,就可以写出像下面这样的C#代码:
public sealed class Program
{
public static void Main()
{
Rational r1 = 5; //Int32 隐式转型为 Rational
Rational r2 = 2.5F; //Single 隐式转型为Rational
Int32 x = (Int32)r1; //Rational 显式转型为Int32
Single s = (Single)r2; //Rational 显式转型为Single
}
}
-
implicit 关键字告诉编译器可以隐式转换; explicit 表示必须显式转换。
-
在 implicit 或 explicit 关键字之后,要指定 operator 关键字告诉编译器该方法是一个转换操作符。
-
在 operator 之后,指定对象要转换成什么为型。在圆括号内,则指定要从什么类型转换。
-
不损失精度时用 implicit ,否则用 explicit .显式转换失败,应该抛出 OverflowException 或者 InvalidOperationException 异常。
8.6 扩展方法
翠花,上代码:
//静态的类
public static class StringBuilderExtensions {
//静态方法,this 关键字
public static Int32 IndexOf(this StringBuilder sb, Char value) {
for (Int32 index = 0; index < sb.Length; index++)
if (sb[index] == value) return index;
return -1;
}
}
现在,就可以像这样使用Int32 index=sb.IndexOf('!')
8.6.1 规则和原则
-
C#只支持扩展方法,不支持扩展属性、扩展事件、扩展操作符等。
-
扩展方法(第一个参数前面有 this 的方法)必须在非泛型的静态类中声明。类名没有限制。至少要有一个参数,而且只有第一个参数能用 this 关键字标记。
-
如果有人在 Wintellect 命名空间中定义了一个 StringBuilderExtensions 类,那么程序员为了访问这个类的扩展方法,必须在他的源代码文件顶部写一条
using Wintellect;
指令。 -
扩展方法可能存在版本控制问题。如果 Microsoft 未来为他们的 StringBuilder 类添加了 IndexOf 实例方法,而且和我的代码调用的原型一样,那么在重新编译我的代码时,编译器会绑定到Microsoft的IndexOf实例方法,而不是我的静态IndexOf方法。这样我的程序就会有不同的行为。
8.6.2 用扩展方法扩展各种类型
- 为接口定义扩展方法
public static void ShowItems<T>(this IEnumerable<T> collection) {
foreach (var item in collection)
Console.WriteLine(item);
}
public static void Main()
{
//每个Char在控制台上单独显示一行
"Grant".ShowItems();
//每个String在控制台上单独显示一行
new[] { "Jeff", "Kristin" }.ShowItems();
//每个Int32在控制台上单独显示一行
new List<Int32>() { 1, 2, 3 }.ShowItems();
}
- 为委托类型定义扩展方法
public static void InvokeAndCatch<TException>(this Action<object> d, object o)
where TException : Exception
{
try { d(o); }
catch (TException) { }
}
public static void Main()
{
//创建一个Action委托(实例)来引用静态 ShowItems 扩展方法。
//并初始化第一个实参来引用字符串“Jeff”
Action a = "Jeff".ShowItems;
//调用(Invoke)委托,后者调用(call)ShowItems.
//并向它传递对字符串“Jeff”的引用
a();
}
8.6.3 ExtensionAttribute类
8.7 分部方法
//工具生成的代码,存储在某个源代码文件中
internal sealed partial class Base
{
private String m_name;
//这是分部方法的声明
partial void OnNameChanging(String value);
public String Name
{
get { return m_name; }
set
{
OnNameChanging(value.ToUpper()); //通知类要进行更改了
m_name = value; //更改字段
}
}
}
//开发人员生成的代码,存储在另一个源代码文件中:
internal sealed partial class Base
{
//这是分部方法的实现,会在m_name更改前调用
partial void OnNameChanging(string value)
{
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException("value");
}
}
规则和原则:
-
它们只能在分部类或结构中声明。
-
分部方法的返回类型始终是 void ,任何参数都不能用 out 修饰符来标记,之所以如此要求,都是因为——方法可能不存在。
-
可以有 ref 参数,可以是泛型方法,可以是实例或静态方法,而且可标记为 unsafe 。
-
分部方法的声明和实现必须具有完全一致的签名。
-
如果没有对应的实现部分,便不能在代码中创建一个委托来引用这个分部方法(编译器会报错)。同样是因为——方法可能不存在。
-
分部方法总是被视为 private 方法,但C#编译器禁止在分部方法声明之前添加 private 关键字。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步