C# 学习笔记(二)

方法

方法的结构和代码执行基本和 C++ 相同

局部变量

除了没有隐式的初始化外,基本和 C++ 相同,值类型的存储在栈中,引用类型的话,引用存储在栈中,数据存储在堆中。

类型推断和 var 关键字

var 关键字基本等同于 C++ 的 auto 效果,是静态的类型推导

  • 只能用于局部变量,不能用于字段
  • 只能在变量声明中包含初始化时使用
  • 一旦编译器推断出变量的类型,它就是固定且不能更改的。
static void Main()
{
    var total = 15; // 等同于 int total = 15;
    var mec = new MyExcellentClass(); // 等同于 MyExcellentClass mec = new MyExcellentClass();
}

嵌套块中的局部变量

类似 C/C++ ,可以在方法内嵌套更多的块,在块内部声明的局部变量,生存期和可见性都只在块范围内

static void Main()
{
    int var1 = 10;
    {
        int var2 = 5;
        Console.WriteLine(var1);
        Console.WriteLine(var2);
    }
}

与 C++ 不同的是,C# 不允许内部嵌套块里有和外部同名的局部变量,C++ 如果有同名的变量,内部的会覆盖外部的,而 C# 编译器会报错。

局部常量

  • 和 C++ 常量相同,使用 const 关键字
  • 和 C++ 不同的是,C# 的 const 关键字不是一个修饰符,而是核心声明的一部分,必须放在类型前面,不像 C++ 一样可以调换位置。
  • 除此之外,C# 的 var 关键字也不能和 const 一起使用,隐式类型化的变量不能是常量。
  • 对于引用类型,只能初始化为 null,new 创建的实例引用是运行时才决定的。

控制流

  • 选择语句
    • if 有条件的执行一条语句
    • if...else 有条件的执行一条或另一条语句
    • switch 有条件的执行一组语句中的某一条
  • 迭代语句
    • for 循环——在顶部测试
    • while 循环——在顶部测试
    • do 循环 —— 在底部测试
    • foreach 为一组中每个成员执行一次
  • 跳转语句
    • break 跳出当前循环
    • continue 到当前循环底部
    • goto 到一个命名的语句
    • return 返回到调用方法继续执行

方法调用和返回

基本和 C++ 调用函数相同,除了返回值类型为 void 的方法,其他都需要通过 return 返回。

局部函数

与 C++ 不同,从 C#7.0 开始,C# 就允许函数嵌套,你可以在函数中声明另一个函数,只要在同一个块中声明,这整个块的范围内都可以调用,不需要考虑顺序。

class Program
{
    public void MethodWithLocalFunction()
    {
        int result = MyLocalFunction(5);
        Console.WriteLine(result);

        int MyLocalFunction(int x)
        {
            return x * 2;
        }
    }

    static void Main()
    {
        Program program = new Program();
        program.MethodWithLocalFunction();
    }
}
// 10

参数

形参和实参

形参和实参的概念基本和 C++ 相同,传递参数时,必须一一对应,同样的也可以发生隐式的类型转换。

值参数

值参数类似于 C++ 中值传递的概念,默认的参数传递都是值传递,即复制实参的值给形参。包括引用类型也是如此,只不过 C# 的引用类型类似于 C++ 的指针,因此,即便是值传递,也是相当于复制了一份引用,修改时还是修改的引用指向的数据,除非像指针一样,重新指向其他的实例。

class MyClass
{
    public int Val = 20;
}

class Program
{
    static void MyMethod(MyClass f1, int f2)
    {
        f1.Val = f1.Val + 5;
        f2 = f2 + 5;
        Console.WriteLine($"f1.Val: {f1.Val}, f2: {f2}");
    }

    static void Main()
    {
        MyClass a1 = new MyClass();
        int a2 = 10;
        MyMethod(a1, a2);
        Console.WriteLine($"a1.Val: {a1.Val}, a2: {a2}");
    }
}
// f1.Val: 25, f2: 15
// a1.Val: 25, a2: 10

引用参数

类似 C++ 的引用传递的概念

  • 必须在方法的声明和调用中都使用 ref 修饰符
  • 实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引用或者 null
  • 值参数,系统会在栈上为形参分配空间,而引用参数不会
  • 形参的参数名将作为实参变量的别名,指向相同的内存位置(参考 C++ 的引用)
class MyClass
{
    public int Val = 20;
}

class Program
{
    static void MyMethod(ref MyClass f1, ref int f2)
    {
        f1.Val = f1.Val + 5;
        f2 = f2 + 5;
        Console.WriteLine($"f1.Val: {f1.Val}, f2: {f2}");
    }

    static void Main()
    {
        MyClass a1 = new MyClass();
        int a2 = 10;
        MyMethod(ref a1, ref a2);
        Console.WriteLine($"a1.Val: {a1.Val}, a2: {a2}");
    }
}
// f1.Val: 25, f2: 15
// a1.Val: 25, a2: 15

引用类型作为值参数和引用参数

基本类似于 C++ 值传递指针时的效果:

class MyClass
{
    public int Val = 20;
}

class Program
{
    static void RefAsParameter(MyClass f1)
    {
        f1.Val = 50;
        Console.WriteLine($"After member assignment: {f1.Val}");
        f1 = new MyClass();
        Console.WriteLine($"After new object creation: {f1.Val}");
    }

    static void Main()
    {
        MyClass a1 = new MyClass();
        Console.WriteLine($"Before method call: {a1.Val}");
        RefAsParameter(a1);
        Console.WriteLine($"After method call: {a1.Val}");
    }
}
// Before method call: 20
// After member assignment: 50
// After new object creation: 20
// After method call: 50

基本类似于 C++ 引用传递指针时的效果:

class MyClass
{
    public int Val = 20;
}

class Program
{
    static void RefAsParameter(ref MyClass f1)
    {
        f1.Val = 50;
        Console.WriteLine($"After member assignment: {f1.Val}");
        f1 = new MyClass();
        Console.WriteLine($"After new object creation: {f1.Val}");
    }

    static void Main()
    {
        MyClass a1 = new MyClass();
        Console.WriteLine($"Before method call: {a1.Val}");
        RefAsParameter(ref a1);
        Console.WriteLine($"After method call: {a1.Val}");
    }
}
// Before method call: 20
// After member assignment: 50
// After new object creation: 20
// After method call: 20

输出参数

用法和效果和引用参数类似,使用 out 修饰符,只有以下几个区别

  • 方法内部,必须给输出参数赋值后才能读取。因此参数的初始值是无关的,且没有必要在方法调用前给对应的实参赋值
  • 和返回值的要求一样,在方法返回前,必须在代码中每条可能的路径中为所有输出参数赋值。
class MyClass
{
    public int Val = 20;
}

class Program
{
    static void MyMethod(out MyClass f1, out int f2)
    {
        f1 = new MyClass();
        f1.Val = 25;
        f2 = 15;
    }

    static void Main()
    {
        MyClass a1 = null;
        int a2;
        MyMethod(out a1, out a2);
        Console.WriteLine(a1.Val); // 25
        Console.WriteLine(a2); // 15
    }
}

从 C# 7.0 开始,你可以不预先声明一个变量来用作 out 参数,可以直接在调用时,在参数列表添加变量类型,将其作为变量声明

static void Main()
    {
        MyMethod(out MyClass a1, out int a2);
        Console.WriteLine(a1.Val); // 25
        Console.WriteLine(a2); // 15
    }

参数数组

有点类似 C++ 可变参数的概念,可以让函数接收不定个数的参数。

  • 在一个参数列表中只能有一个参数数组
  • 如果有,它必须是列表中的最后一个
  • 参数数组表示的所有参数必须是同一类型

声明方式:

  • 在数据类型前使用 params 修饰符
  • 在数据类型后放置一组空的方括号
void ListInts(params int[] inVals) // 形参 inVals 表示零个或多个 int 实参
{
    ...
}

方法调用

有两种方式为参数数组提供实参

  • 一个逗号分隔的该数据类型元素的列表。所有元素必须是方法声明中的指定的类型

    ListInts(10, 20, 30);
    
  • 一个该数据类型元素的一维数组

    int [] intArray = {1, 2, 3};
    ListInts(intArray);
    

params 与前面几种参数类型不同,只在声明中添加修饰符,调用时不允许添加

ref 局部变量和 ref 返回

ref 局部变量就是 C++ 的引用变量,相当于给一个局部变量创建一个别名,使用时需要使用两次 ref 关键字

class Program
{
    static void Main()
    {
        int x = 2;
        ref int y = ref x;
        Console.WriteLine($"x= {x}, y= {y}");
        x = 5;
        Console.WriteLine($"x= {x}, y= {y}");
        y = 6;
        Console.WriteLine($"x= {x}, y= {y}");
    }
}
// x= 2, y= 2
// x= 5, y= 5
// x= 6, y= 6

而 ref 返回就是相当于 C++ 函数返回了一个引用

class Simple
{
    private int Score = 5;

    public ref int RefToScore()
    {
        return ref Score;
    }

    public void Display()
    {
        Console.WriteLine($"Score: {Score}");
    }
}

class Program
{
    static void Main()
    {
        Simple s = new Simple();
        s.Display();
        ref int v1Outside = ref s.RefToScore();
        v1Outside = 10;
        s.Display();
    }
}
// Score: 5
// Score: 10
  • 显然,返回类型为 void 的方法是不能声明为 ref 返回方法的

  • 除此之外,ref return 表达式不能返回如下内容:

    • 空值
    • 常量
    • 枚举成员
    • 类或者结构体的属性
    • 指向只读位置的指针
  • ref return 只能指向原先就在调用域内的位置,或者字段。所以不能指向方法内的局部变量

  • ref 局部变量只能被赋值一次,一旦初始化,就不能指向不同的存储位置了

  • 即使将一个方法声明为 ref 返回方法,如果在调用该方法时省略了 ref 关键字,则返回的是值,而不是指向值的内存位置的指针

    int v1Outside = s.RefToScore();
    v1Outside = 10;
    s.Display();// Score: 5
    
  • 如果将 ref 局部变量作为常规的实参传递给其他方法,依旧是值传递(与 C++ 的引用变量一样)

方法重载

同 C++ 的函数重载,使用相同名称的每个方法必须有一个和其他方法不同的签名,签名由以下信息组成

  • 方法的名称
  • 参数的数目
  • 参数的数据类型和顺序
  • 参数修饰符

和 C++ 一样返回值类型和形参名称都不是签名的一部分

命名参数

C# 允许像 python 一样显式的指定参数的名字来以任意顺序在方法中列出实参

在调用时,你可以同时使用位置参数和命名参数,但所有位置参数必须先列出

class MyClass
{
    public int Calc(int a, int b, int c)
    {
        return (a + b) * c;
    }
}

class Program
{
    static void Main()
    {
        MyClass mc = new MyClass();

        int r0 = mc.Calc(4, 3, 2);
        int r1 = mc.Calc(4, b: 3, c: 2);
        int r2 = mc.Calc(4, c: 2, b: 3);
        int r3 = mc.Calc(c: 2, b: 3, a: 4);
        int r4 = mc.Calc(c: 2, b: 1 + 2, a: 3 + 1);
        int r5 = mc.Calc(4, b: 3, 2); // 正常来说命名参数后面是不能有位置参数的,但如果剩下的刚好是最后一个参数,就可以
        int r6 = mc.Calc(c: 2, b: 3, 4); // 这个就会报错了

        Console.WriteLine($"{r0}, {r1}, {r2}, {r3}, {r4}, {r5}"); // 全是 14
    }
}

可选参数

同 C++ 默认参数,在方法声明时,设定好默认值

  • 不是所有类型都可以作为可选参数

    • 只要值类型的默认值可以在编译时确定,就可以使用值类型作为可选参数
    • 引用类型只有默认值为 null 时,可以在编译时确定,因此只能设置为 null
    • ref、out、params 参数都不能作为可选参数
  • 和 C++ 一样必填参数必须在可选参数之前声明。如果有 params 参数,必须在所有可选参数之后声明

  • 使用位置参数时,和 C++ 一样只能从后往前省略

    class MyClass
    {
        public int Calc(int a = 2, int b = 3, int c = 4)
        {
            return (a + b) * c;
        }
    }
    
    class Program
    {
        static void Main()
        {
            MyClass mc = new MyClass();
    
            int r0 = mc.Calc(5, 6, 7); // 使用所有显式值
            int r1 = mc.Calc(5, 6); // 为 c 使用默认值
            int r2 = mc.Calc(5); // 为 b 和 c 使用默认值
            int r3 = mc.Calc(); // 使用所有默认值
    
            Console.WriteLine($"{r0}, {r1}, {r2}, {r3}"); // 77, 44, 32, 20
        }
    }
    
  • 配合命名参数就可以随意省略可选参数了

    class MyClass
    {
        public double GetCylinderVolume(double r = 3.0, double h = 4.0)
        {
            return 3.1416 * r * r * h;
        }
    }
    
    class Program
    {
        static void Main()
        {
            MyClass mc = new MyClass();
            double volume;
    
            volume = mc.GetCylinderVolume(3.0, 4.0);
            Console.WriteLine(volume); // 113.0976
            volume = mc.GetCylinderVolume(r: 2.0);
            Console.WriteLine(volume); // 50.2656
            volume = mc.GetCylinderVolume(h: 2.0);
            Console.WriteLine(volume); // 56.5488
            volume = mc.GetCylinderVolume();
            Console.WriteLine(volume); // 113.0976
        }
    }
    

栈帧和递归

和 C++ 没啥区别,不多赘述

深入理解类

类成员

  • 数据成员
    • 字段
    • 常量
  • 函数成员
    • 方法
    • 属性
    • 构造函数
    • 析构函数
    • 运算符
    • 索引
    • 事件

成员修饰符的顺序

类成员声明语句由下列部分组成,方括号代表可选

[特性] [修饰符] 核心声明

  • 多个修饰符可以任意顺序排列,但必须在核心声明前面(书上说 const 是核心声明的一部分,但这里又放到了修饰符里,但可以确定的是 const 后面必须是类型,不能和其他修饰符交换位置)
  • 多个特性也可任意顺序排列,但必须在修饰符和核心声明前面

实例类成员

和 C++ 一样,普通的字段都是所有实例各自独立的

静态字段

和 C++ 类似,通过 static 关键字修饰的字段为静态字段,整个类共享,只能通过类名访问,不能通过实例来访问

生存期也是和 C++ 一样,即便没有类的实例,其静态成员也依旧存在

class D {
    static int Mem1;
}
// 通过 D.Mem1 访问

静态函数成员

同 C++,只能访问类的静态成员,不能访问实例成员

其他静态类成员类型

前面提到的九种类成员,只有常量和索引器不能声明为 static

成员常量

基本和局部常量一样,只不过一个声明在方法内一个声明在类声明中,必须初始化

常量与静态量

与 C++ 的常成员变量不同的是,C# 的成员常量具有静态成员的特性,对类的每个实例都可见,即便没有类的实例,也可以直接通过类名来访问。与静态量不同,常量没有自己存储位置,而是在编译时被编译器替换,类似于 C/C++ 的 #define

属性

属性在写入和读取时,和普通字段语法相同,它们的区别在于属性是一个函数成员

  • 它一定为数据存储分配空间
  • 它执行代码

属性是一组(两个)匹配的、命名的、成为访问器的方法

  • set 访问器为属性赋值
  • get 访问器从属性获取值

属性声明和访问

  • set 访问器总是:
    • 拥有一个单独的、隐式的值参,名称为 value,与属性类型相同
    • 拥有一个返回类型 void
  • get 访问器总是:
    • 没有参数
    • 拥有一个和属性类型相同的返回类型

set 和 get 可以交换顺序,但属性上不能有这两个访问器之外的其他访问

class C1
{
    private int theRealValue;

    public int MyValue
    {
        set { theRealValue = value; }
        get { return theRealValue; }
    }
}

class Program
{
    static void Main()
    {
        var c = new C1();
        c.MyValue = 5;
        Console.WriteLine(c.MyValue); // 5
    }
}

属性只能通过字段的形式来使用,不能显式的调用 set 和 get 访问器

属性和关联字段

属性通常与字段关联,一种常见的方式是在类中将字段以 private 封装,并声明一个 public 属性来从类的外部对该字段访问,如上面这个样例。和属性关联的字段常称为后备字段后备存储

属性和它们的后备字段有几种命名约定:

  • 一种是两个名称使用相同的内容,但字段使用 Camel 大小写(小驼峰),属性使用 Pascal 大小写(大驼峰)
  • 另一种约定是属性使用 Pascal 大小写,字段使用相同标识符的 Camel 大小写版本,并以下划线开始。
private int firstField;
public int FirstField
{
    set { firstField = value; }
    get { return firstField; }
}

private int _secondField;
public int SecondField
{
    set { _secondField = value; }
    get { return _secondField; }
}

执行其他计算

属性访问器不局限于对关联的后备字段传进传出数据。get 和 set 可以执行任何计算

public int Useless 
{ 
    set { } // 什么也不设置 
    get { return 5; } // 只是返回值 5 
}

更实用的例子

private int theRealValue;
public int MyValue
{
    set { theRealValue = value > 100 ? 100 : value; }
    get { return theRealValue; }
}

C# 7.0 为属性的 getter/setter 引入了另一种语法,这种语法使用表达函数体(lambda 表达式),这种语法只有在访问函数体由一个表达式组成时才可以使用,形式上更类似于 JavaScript 的箭头函数

private int theRealValue;
public int MyValue
{
    set => theRealValue = value > 100 ? 100 : value; // 书中这里少了赋值部分,貌似错了
    get => theRealValue;
}

只读和只写属性

  • 只有 get 访问器的属性称为只读属性。只读属性能够安全地将一个数据项从类或类的实例中传出,而不必让调用者修改属性值。
  • 只有 set 访问器的属性称为只写属性。只写属性很少见,因为它们几乎没有实际用途。如果想在赋值的时候触发一个副作用,应该使用方法而不是属性。
  • 两个访问器中至少有一个必须定义,否则编译器会产生一条错误消息。

属性与共有字段

按照推荐的编码实践,属性比共有字段更好,理由如下。

  • 属性是函数成员而不是数据成员,允许你处理输入和输出,而共有字段不行。
  • 属性可以只读或者只写,而字段不行。(额,字段貌似可以只读)
  • 编译后的变量和编译后的属性语义不同。

如果要发布一个由其他代码引用的程序集,那么第三点将会带来一些影响。例如,有的时候开发人员可能想用共有字段代替属性,因为如果以后需要为字段的数据增加逻辑的话,可以再把字段改为属性。这没错,但是如果那样修改的话,所有访问这个字段的其他程序集都需要重新编译,因为字段的属性和编译后的语义不一样。另外如果实现的是属性,那么只需要修改属性的实现,而无需编译访问它的其他程序集。

自动实现属性

因为属性经常被关联到后备字段,C# 提供了自动实现属性,常简称为“自动属性”,允许只声明属性而不声明后备字段。编译器会为你创建隐藏的后备字段,并且自动挂接到get和set访问器上。

  • 不声明后备字段——编译器根据属性的类型分配存储。
  • 不能提供访问器的方法体——它们必须被简单地声明为分号。
    • get担当简单的内存读,set担当简单的写。
    • 但是因为无法访问自动属性的方法体,所以在使用自动属性时调试代码通常会更加困难。
    • 从 C#6.0 开始,可以使用只读属性了。此外还可以将自动属性初始化作为其声明的一部分。
class C1
{
    public int MyValue { set; get; }
}

class Program
{
    static void Main()
    {
        var c = new C1();
        Console.WriteLine(c.MyValue); // 0
        c.MyValue = 20;
        Console.WriteLine(c.MyValue); // 20
    }
}

静态属性

属性也可以声明为static。静态属性的访问器和所有静态成员一样,具有以下特点

  • 不能访问类的实例成员,但能被实例成员访问
  • 不管类是否有实例,它们都是存在的
  • 在类的内部,可以仅使用名称来引用静态属性
  • 在类的外部,正如本章前面描述的,可以通过类名或者使用 using static 结构来应用静态头三行,即使没有类的实例,也能访问属性。Main的最后一行调用一个实例方法,它从类的内部访问属性
using System;
using static ConsoleTestApp.Trivial;
namespace ConsoleTestApp
{
    class Trivial
    {
        public static int MyValue { get; set; }

        public void PrintValue()
        {
            Console.WriteLine($"Value from inside:{MyValue}"); // 类的内部访问
        }
    }

    class Program
    {
        static void Main()
        {
            Console.WriteLine($"Init Value: {Trivial.MyValue}");
            Trivial.MyValue = 10; //  类的外部访问
            Console.WriteLine($"Init Value: {Trivial.MyValue}");
            MyValue = 20; // 类的外部访问,但由于使用了 using static,所以没有使用类名
            Console.WriteLine($"New Value : {MyValue}");
            Trivial trivial = new Trivial();
            trivial.PrintValue();
        }
    }
}
// Init Value: 0
// Init Value: 10
// New Value : 20
// Value from inside:20

实例构造函数

  • 基本等同于 C++ 的构造函数
  • 和 C++ 的构造函数一样可以有各自带参数的重载
  • 如果没有显式提供实例构造函数,就会提供一个隐式的默认构造函数,没有参数,方法体为空
  • 只要有一个显式的构造函数,编译器就不会再创建额外的默认构造函数,此时如果调用无参的默认构造函数可能就会报错

静态构造函数

与 C++ 不同的是,C# 的构造函数可以声明为 static。通常用于初始化类的静态字段

  • 静态构造函数在以下方面与实例构造函数不同
    • 函数声明中使用 static 关键字
    • 只能有一个静态构造函数,而且不能带参数
    • 不能有访问修饰符
  • 除此之外
    • 类既可以有实例构造函数,也可以有静态构造函数
    • 静态构造函数和静态方法一样,不能访问实例成员,也不能使用 this 访问器
    • 不能在程序中显式调用静态构造函数,系统会自动调用:
      • 在引用任何静态成员之前
      • 在创建类的任何实例之前

对象初始化语句

类似于 C++ 的初始化列表,C# 也可以在使用 new 创建类时,通过大括号的形式来初始化参数

不过需要注意的是创建对象的代码必须能够访问要初始化的字段和属性,且初始化发生在构造方法执行之后,构造方法中设置的值可能被之后的对象初始化覆盖

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 }; // 使用初始化语句时,可以省略圆括号
        Console.WriteLine("pt1: {0}, {1}", pt1.X, pt1.Y);
        Console.WriteLine($"pt2: {pt2.X}, {pt2.Y}");
    }
}
// pt1: 1, 2
// pt2: 5, 6

析构函数

析构函数执行在类的实例被销毁前需要的清理或释放非托管资源的行为。非托管资源指通过 Win32 API 获得的文件句柄,或非托管内存块。使用 .NET 资源是无法得到它们的,因此如果坚持使用 .NET 类,就不需要为类编写析构函数。用法基本和 C++ 的相同

readonly 修饰符

字段可以用 readonly 修饰符声明。作用类似与 const,一旦被设定就不能改变,readonly 字段更接近于 C++ 中的常成员变量

  • const 字段只能在声明语句中初始化,而 readonly 字段可以在以下任何位置设置它的值
    • 字段声明语句,同 const
    • 类的任何构造函数。如果是 static 字段就必须在静态构造函数中完成
  • const 字段的值必须在编译时决定,而 readonly 字段的值可以在运行时决定
  • const 的行为总是静态的,而对于 readonly 字段以下两点是正确的
    • 它可以是实例字段,也可以是静态字段
    • 它在内存中有存储位置
class Shape
{
    readonly double PI = 3.1416;
    readonly int NumOfSides;

    public Shape(double side1, double side2)
    {
        NumOfSides = 4;
    }

    public Shape(double side1, double side2, double side3)
    {
        NumOfSides = 3;
    }
}

this 关键字

基本和 C++ this 指针相同,只能被用于

  • 实例构造函数
  • 实例方法
  • 属性和索引器的实例访问器

索引器

有的时候我们会希望像数组一样访问类的字段,因此就有了索引器,其更类似 C++ 重载 [] 运算符

  • 索引器类似于属性,是一组 get 和 set 访问器
  • 和属性一样,索引器不用分配内存来存储
  • 属性通常表示单个数据成员,索引器通常表示多个数据成员
  • 和属性一样,索引器可以只有一个访问器,也可以两个都有
  • 索引器总是实例成员,不能声明为 static
  • 和属性一样,实现 get 和 set 的代码不一定要关联到某个字段或属性

声明索引器

  • 索引器没有名称,在名称的位置是关键字 this
  • 参数列表在方括号中间
  • 参数列表中必须至少声明一个参数

ReturnType this [Type param1, ...] {

get {...}
set {...}

}

set 访问器被调用时接受两项数据

  • 一个名为 value 的隐式参数,其中持有要保存的数据
  • 一个或更多索引参数,表示数据应该保存到哪里

和属性相同,索引器也无法显式调用 set 和 get

class Employee
{
    public string LastName;
    public string FirstName;
    public string CityOfBirth;

    public string this[int index]
    {
        get
        {
            switch (index)
            {
                case 0: return LastName;
                case 1: return FirstName;
                case 2: return CityOfBirth;
                default: throw new ArgumentOutOfRangeException("index");
            }
        }
        set
        {
            switch (index)
            {
                case 0: LastName = value; break;
                case 1: FirstName = value; break;
                case 2: CityOfBirth = value; break;
                default: throw new ArgumentOutOfRangeException("index");
            }
        }
    }
}

class Program
{
    static void Main()
    {
        Employee employee = new Employee();
        employee[0] = "Smith";
        employee[1] = "John";
        employee[2] = "New York";

        Console.WriteLine($"Last Name: {employee[0]}");
        Console.WriteLine($"First Name: {employee[1]}");
        Console.WriteLine($"City of Birth: {employee[2]}");
    }
}

索引器重载

只要索引器的参数列表不同,类就可以有任意多个索引器

访问器的访问修饰符

默认情况下成员的两个访问器的访问级别是和成员自身相同的,但你也可以分配不同的访问级别

class Person
{
    public string Name { get; private set; }
    public Person(string name) {Name = name;}
}
class Program
{
    static void Main()
    {
       Person p = new Person("John");
       Console.WriteLine(p.Name);
    }
}

访问器的访问修饰符有几个限制

  • 仅当成员既有 get 访问器又有 set 访问器时,其访问器才能有访问修饰符
  • 虽然两个访问器都必须出现,但只能有一个有访问修饰符
  • 访问器的访问修饰符的限制必须比成员的访问级别更严格

image-20250503003944695

分部类和分部类型

类的声明可以分割成几个分部类的声明

  • 每个分部类都含有一些类成员的声明
  • 类的分部类声明可以在同一个文件也可以不在
  • 每个分部类声明必须标注为 partial class,而不是单独的关键字 class
partial class MyPartClass
{
    public int x;
}

partial class MyPartClass
{
    public int y;
}

class Program
{
    static void Main()
    {
        MyPartClass myPartClass = new MyPartClass();
        myPartClass.x = 10;
        myPartClass.y = 20;
        Console.WriteLine($"x: {myPartClass.x}, y: {myPartClass.y}");
    }
}

partial 不是关键字,只是类型修饰符,因此可以当作标识符,但直接用在 class、struct 或 interface之前时,表示分部类型

组成类的所有分部类声明必须在一起编译

分部方法

分部方法是声明在分部类中不同部分的方法。有些类似于 C++ 把函数声明和函数定义分开来的写法,分部方法的不同部分可以声明在分部类的不同部分,也可以声明在同一个部分,其包括两部分:

  • 定义分部方法声明
    • 给出签名和类型
    • 声明的实现部分只是一个分号
  • 实现分部方法声明
    • 给出签名和返回类型
    • 以普通的语句块形式实现

分布方法有以下几点

  • 定义声明和实现声明的签名和返回类型必须匹配
    • 返回类型必须是 void
    • 签名不能包含访问修饰符,这使分部方法是隐式私有的
    • 参数列表不能包含 out 参数
    • 在定义声明和实现声明中都必须包含上下文关键字 partial,并且直接放在关键字 void 之前
  • 可以有定义部分而没有实现部分。这种情况下,编译器把方法的声明以及方法内部任何对方法的调用都移除。不能只有分部方法的实现部分而没有定义部分
partial class MyClass
{
    partial void PrintSum(int x, int y);

    public void Add(int x, int y)
    {
        PrintSum(x, y);
    }
}

partial class MyClass
{
    partial void PrintSum(int x, int y)
    {
        Console.WriteLine($"The sum is: {x + y}");
    }
}

class Program
{
    static void Main()
    {
        MyClass myClass = new MyClass();
        myClass.Add(5, 6);
    }
}