跳转至

类与对象⚓︎

一、命名空间⚓︎

01 声明和使用⚓︎

命名空间是指包含一组相关对象的作用域(可以理解为 java 中的 package)。

  • 声明:namespace 关键字用来声明命名空间。
  • 使用:using 关键字用来引入命名空间。

示例:

using System;

// 命名空间
namespace HelloWorld
{
    // 类名
    internal class Program
    {
        // main函数
        static void Main(string[] args)
        {
            // 打印语句
            Console.WriteLine("Hello, World!");
        }
    }
}

02 顶级语句⚓︎

C#10允许顶级语句,可以省略大括号,用分号代替,称为文件范围的命名空间。

示例:

using System;

// 命名空间
namespace HelloWorld;

// 类名
internal class Program
{
    // main函数
    static void Main(string[] args)
    {
        // 打印语句
        Console.WriteLine("Hello, World!");
    }
}

03 注意事项⚓︎

  1. 一般每个文件只有一个命名空间
  2. 同一命名空间下的类名不能相同
  3. 高级语句不允许命名空间嵌套

二、类⚓︎

01 类的定义⚓︎

类是对具有共同特征的事物的抽象。

类的声明:

```CSharp
access_specifier class class_name
{
    access_ specifier data_type member;

    access specifier return_type method(parameter_list)  
    {  
        // 函数体  
    }
}

示例:

public class Student
{
    private string name;
    private string age;

    public Student(string name, string age)
    {
        this.name = name;
        this.age = age;
    }

    public string GetStudent()
    {
        // todo something
        return $"{this.name}" + ":" + $"{this.age}";
    }
}

02 封装继承多态⚓︎

封装:为了防止对实现细节的访问,把一个或多个项目封闭在一个物理的或者逻辑的包中。

成员访问修饰符: - public 可供外部类访问 - private 仅供内部成员函数访问 - protected 供内部和子类访问 - internal 允许命名空间内访问 - protected internal 允许命名空间内访问,且允许子类访问 - private protected 仅供子类访问,C#7.2及以上版本才支持该修饰符

继承:通过继承可以在创建新类时重用、扩展和修改被继承类中定义的成员。 C#不允许类的多继承.

class ClassName : ParentClassName
{
    // todo
}

多态:意味着有多重形式,在面向对象编程范式中,多态性往往表现为"一个接口,多个功能"。

Shape shape = new Rectangle(); // class Rectangle : Shape{}
Shape shape = new Triangle(); // class Triangle : Shape{}
Shape shape = new Circle(); // class Circle : Shape{}

03 类成员⚓︎

类的成员包含: - 常量 - 字段 - 属性 - 方法 - 构造函数 - 终结器 - 重载运算符 - 嵌套类 - 事件 - 索引器

04 静态成员与静态类⚓︎

静态成员属于类型本身而不是属于特定对象。

类的静态成员可以是: 1. 静态成员变量 2. 静态成员函数 3. 静态构造函数

声明:在对应的成员前使用static关键字进行修饰。

访问: 1. 静态成员不属于对象,因此请使用类名访问它:ClassName.static_memeber 2. 静态函数只能访问静态成员,不能访问非静态成员。因为静态成员属于类,会比非静态优先加载,如果静态函数访问非静态成员会找不到尚未加载的非静态成员。 3. 静态成员只会被加载一次,只有一个副本

定义静态类:通过 static 关键字声明的类为静态类,静态类与非静态类基本相同,但静态类无法实例化。

示例:

静态类定义
public static class TemperatureConverter
{
    // Static Memebers
}

Note

静态类是密封的,不能被继承。

三、类成员⚓︎

01 字段⚓︎

声明与访问⚓︎

字段是在类或结构中直接声明的任意类型的变量。

字段分为实例字段和静态字段

  • 实例字段:属于对象,该类型的实例私有,需要通过实例名访问。
  • 静态字段:属于类,在所有该类型的实例之间共享,需要通过类名访问。

声明与访问:

// 声明
private DateTime _date;  // 实例字段
public static int year; // 静态字段

// 声明并赋初始值
public string Day = "Monday";

// 访问
CalendarEntry birthday = new CalendarEntry(); 
birthday.Day = "Saturday";

注意事项:

  1. 字段初始化时不能引用其他实例字段;
  2. 字段会在对象实例的构造函数被调用之前即刻初始化;
  3. 如果构造函数分配了字段的值,则它将覆盖在字段声明期间给定的任何值。

只读字段与必须字段⚓︎

只读字段:使用readonly将字段设为只读,只能在初始化期间或在构造函数中为只读字段赋值。

static readonly 字段类似于常量,只不过 C# 编译器在编译时不具有对静态只读字段的值的访问权限,而只有在运行时才具有访问权限。

必须字段:使用required将字段设为必须,必填字段必须由构造函数初始化,或者在创建对象时由对象初始值设定项初始化。

不能在同一字段上组合 required 修饰符和 readonly 修饰符。

常量字段⚓︎

常量是不可变的值,在编译时是已知的,在程序的生命周期内不会改变。

常量类型声明规则:

  • System.Object 外的C#内置类型都可声明为常量;
  • 用户自定义类型(包括类、结构和数组)不可声明为常量;
  • C# 不支持 const 方法、属性或事件;
  • 因此 const 字段不能通过引用传递,并且不能在表达式中显示为左值。

    常量没有与之相关量的内存地址,当编译器遇到 C# 源代码中的常量标识符时,它直接将文本值替换到它生成的中间语言 (IL) 代码中。

声明:

class Calendar
{
    // 使用const关键字修饰
    public const int Months = 12;

    // 同时声明多个常量字段
    public const int Months = 12, Weeks = 52, Days = 365;

    // 常量字段可以在初始化时引用另一个常量字段
    public const double DaysPerWeek = (double) Days / (double) Weeks;
}

02 属性⚓︎

属性是一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。属性可用作公共数据成员,但它们是称为“访问器”的特殊方法。 此功能使得可以轻松访问数据,还有助于提高方法的安全性和灵活性。

访问器⚓︎

访问器:

  • get :用来返回属性值,必须以 returnthrow 结尾;
  • set :用来设置新值,value 关键字用于定义由 set 或 init 访问器分配的值。
  • init :仅用于在对象构造过程中(构造器或对象初始值设定项)分配新值(C#9及更高)

    访问器可以使用访问修饰符,如:protected set { _name = value; }

通过访问器的设置,可以将属性分为三类:

  • 读写属性:具有 getset
  • 只读属性:仅具有 get,在 C#9及更高版本中,可以使用 init 代替 set 将属性设为只读;
  • 只写属性:仅具有 set ,只写属性很少出现,常用于限制对敏感数据的访问。

属性定义与使用⚓︎

有一个实现属性的基本模式,该模式使用私有支持字段来设置和检索属性值。get 访问器返回私有字段的值,set 访问器在向私有字段赋值之前可能会执行一些数据验证。

属性的定义规则:

  • 属性可以使用访问修饰符
  • 属性可以在接口中定义
  • 属性可以定义为staticvirtualsealedoverrideabstract

  • 定义和使用示例:

    // 时间间隔类
    public class TimePeriod
    {
        // 字段
        private double _seconds;
    
        // 属性:将秒换算成时
        public double Hours
        {
            // get 属性访问器
            get
            { 
                return _seconds / 3600; 
            }
            // set 属性访问器
            set
            {
                if (value < 0 || value > 24)
                    throw new ArgumentOutOfRangeException(nameof(value),
                          "The valid range is between 0 and 24.");
                _seconds = value * 3600;
            }
        }
    
        private static void Main(string[] args)
        {
            TimePeriod t = new TimePeriod();
            // The property assignment causes the 'set' accessor to be called.
            t.Hours = 24;
    
            // Retrieving the property causes the 'get' accessor to be called.
            Console.WriteLine($"Time in hours: {t.Hours}");
            // The example displays the following output:
            // Time in hours: 24
        }
    }
    

  • 表达式主体:属性访问器通常由单行语句组成,这些语句只分配或只返回表达式的结果,此时可以使用表达式主体进行代码简化,表达式主体使用 => 进行定义。

public class SaleItem
{
    string _name;
    decimal _cost;

    // 表达式主体
    public decimal Price
    {
        get => _cost;
        set => _cost = value;
    }
    // 只读属性可以进一步简化
    public string Name => $"{name}";
}
  • 自动实现的属性:在某些情况下,属性 get 和 set 访问器仅向支持字段赋值或仅从其中检索值,而不包括任何附加逻辑。 通过使用自动实现的属性,既能简化代码,还能让 C# 编译器透明地提供支持字段。
public class SaleItem
{
    public string Name
    { get; set; }

    public decimal Price
    { get; set; }
}
  • 必须的属性:从 C# 11 开始,可以添加 required 成员以强制客户端代码初始化属性或字段。
public class SaleItem
{
    public required string Name
    { get; set; }

    public required decimal Price
    { get; set; }
}

// 若要创建 `SaleItem`,必须使用对象 初始值设定项 设置 `Name` 和 `Price` 属性
var item = new SaleItem { Name = "Shoes", Price = 19.95m };
Console.WriteLine($"{item.Name}: sells for {item.Price:C2}");

03 函数/方法⚓︎

基本概念⚓︎

  • 方法签名:通过指定访问级别(如 public 或 private)、可选修饰符(如 abstract 或 sealed)、返回值、方法的名称以及任何方法参数,在类、结构或接口中声明方法。 这些部件一起构成方法的签名。

    出于方法重载的目的,方法的返回类型不是方法签名的一部分。 但是在确定委托和它所指向的方法之间的兼容性时,它是方法签名的一部分。

  • 形参与实参:函数定义时括号内的参数称为形参;函数调用时实际传入的参数称为实参。
  • 参数传递:根据数据类型的不同,参数传递分为按值传递和按引用传递。
  • 返回值:如果列在方法名之前的返回类型不是 void,则该方法可通过使用return 语句返回值。
  • 表达式主体:使用 => 实现表达式主体以简化代码。
  • 本地函数:可以理解为方法中的方法,参考资料:C# 中的本地函数

本地函数⚓︎

参考资料:C# 中的本地函数

命名参数与可选参数⚓︎

04 构造函数与析构函数⚓︎

C#的构造函数有三种: - 实例构造函数:用来创建对象,需要public修饰 - 静态构造函数:初始化类中的静态数据或执行仅需执行一次的特定操作 - 私有构造函数:供类内部调用,如果不指定权限修饰符,则默认为private

实例构造函数⚓︎

声明:

public ClassName(parameter_list)
{
    // todo
}

注意事项:

  1. 默认无参构造:如果不声明构造函数,C#会默认隐式地生成无参构造,实例化时成员属性会被赋类型默认值。
  2. 有参构造:声明构造函数时可以设置任意参数,构造函数允许重载。

静态构造函数⚓︎

声明:

static ClassName()
{
    // todo
}

功能作用:静态构造函数用于初始化类中的静态数据执行仅需执行一次的特定操作

调用时机:将在创建第一个实例引用类中的静态成员==之前==将自动调用静态构造函数来初始化类。

静态构造函数具有以下特性:

  1. 静态构造函数不使用访问权限修饰符修饰,不具有参数
  2. 类或结构体中只能有一个静态构造函数
  3. 静态构造函数不能继承或重载
  4. 静态构造函数不能直接调用,仅可以由公共语言运行时 (CLR) 调用
  5. 用户无法控制程序中静态构造函数的执行时间

私有构造函数⚓︎

声明:

// 无权限修饰符时,默认为私有private
ClassName()
{
    // todo
}

private ClassName()
{
    // todo
}

注意事项:

  1. 通常用在只包含静态成员的类中,如果一个类中只有私有构造函数而没有公共构造函数的话,那么其他类(除嵌套类外)则无法创建该类的实例。
  2. 空的私有构造函数可阻止自动生成无参数构造函数。

析构函数⚓︎

析构函数主要用于在垃圾回收器回收类实例时执行一些必要的清理操作。

声明:

// 析构函数
~ClassName()   
{  
// todo
}

C# 中的析构函数具有以下特点:

  1. 析构函数只能在类中定义,不能用于结构体
  2. 一个类中只能定义一个析构函数;
  3. 析构函数不能继承或重载
  4. 析构函数没有返回值
  5. 析构函数是自动调用的,不能手动调用;
  6. 析构函数不能对外公开,不能使用访问权限修饰符修饰,也不能包含参数
  7. 析构函数不能是静态

四、函数参数传递⚓︎

01 值传递⚓︎

当值类型作参数时,会将实参的值传递给形参,方法内对形参的修改不影响方法外部的变量。

using System; 
namespace CalculatorApplication
{ 
    class NumberManipulator
    { 
        // 对于基本数据类型,采用值传递方式
        // swap方法内实际上是对x、y的操作,不影响外部实参
        public void swap(int x, int y)
        { 
            int temp; temp = x;
            x = y;
            y = temp;
        } 

        static void Main(string[] args)
        { 
            NumberManipulator n = new NumberManipulator();
            int a = 100; 
            int b = 200; 
            Console.WriteLine("在交换之前,a 的值: {0}", a); 
            Console.WriteLine("在交换之前,b 的值: {0}", b); 
            n.swap(a, b); // 交换
            Console.WriteLine("在交换之后,a 的值: {0}", a); 
            Console.WriteLine("在交换之后,b 的值: {0}", b); 
            Console.ReadLine(); 
        } 
    } 
}

// 输出:
// 在交换之前,a 的值:100 
// 在交换之前,b 的值:200 
// 在交换之后,a 的值:100 
// 在交换之后,b 的值:200

02 引用传递⚓︎

当引用类型作参数时,会将实参的地址传递给形参,方法内对形参的修改会直接影响实参本身。

string和array都是引用类型,但string会有值传递的效果。

using System;

namespace RefTest
{
    class Ref
    {
        static void SquareIt(ref int x)
        // The parameter x is passed by reference.
        // Changes to x will affect the original value of x.
        {
            x *= x;
            System.Console.WriteLine("The value inside the method: {0}", x);
        }

        static void main(string[] args)
        {
            int n = 5;
            System.Console.WriteLine("The value before calling the method: {0}", n);

            SquareIt(ref n);  // Passing the variable by reference.
            System.Console.WriteLine("The value after calling the method: {0}", n);

            // Keep the console window open in debug mode.
            System.Console.WriteLine("Press any key to exit.");
            System.Console.ReadKey();
        }
    }
}

// Output:
// The value before calling the method: 5
// The value inside the method: 25
// The value after calling the method: 25

03 ref : 按引用传递的值类型⚓︎

想要将值类型按引用传递,即传递变量的地址,可以使用ref关键字实现按引用类型传递值类型。

using System;

namespace RefTest
{
    class Ref
    {
        static void SquareIt(ref int x)
        // The parameter x is passed by reference.
        // Changes to x will affect the original value of x.
        {
            x *= x;
            System.Console.WriteLine("The value inside the method: {0}", x);
        }

        static void main(string[] args)
        {
            int n = 5;
            System.Console.WriteLine("The value before calling the method: {0}", n);

            SquareIt(ref n);  // Passing the variable by reference.
            System.Console.WriteLine("The value after calling the method: {0}", n);

            // Keep the console window open in debug mode.
            System.Console.WriteLine("Press any key to exit.");
            System.Console.ReadKey();
        }
    }
}

// Output:
// The value before calling the method: 5
// The value inside the method: 25
// The value after calling the method: 25

ref 不仅可以用来修饰入参,还可以用来返回。如果在方法签名中使用 ref 关键字且其跟随每个 return 关键字,值将按引用返回到调用方,如:

public ref double GetEstimatedDistance()
{
    return ref estDistance;
}

04 in : 只读的引用传递⚓︎

in 关键字会导致参数按引用传递,但方法内不允许对该参数进行修改。

using System;
namespace InTest
{
    class In
    {
        static void InArgExample(in int number)
        {
            // Uncomment the following line to see error CS8331
            // number = 19; // 编译报错,in参数不允许修改
        }

        static void main(string[] args)
        {
            int readonlyArgument = 44;
            InArgExample(readonlyArgument);
            Console.WriteLine(readonlyArgument);     // value is still 44
        }
    }
}

05 out : 必写的引用传递⚓︎

out关键字会导致参数按引用传递,但是方法内必须对该参数进行修改。

示例:

using System;
namespace OutTest
{
    class Out
    {
        static void OutArgExample(out int number)
        {
            number = 44; //注释掉会报错,必须对参数进行赋值操作
        }
        static void main(string[] args)
        {
            int initializeInMethod;
            OutArgExample(out initializeInMethod);
            Console.WriteLine(initializeInMethod); // value is now 44
        }
    }
}

Tip

  1. 使用 out 参数声明方法是返回多个值的经典解决方法,可以使用元组保存返回值。
  2. ref 和 out 的区别: 函数内部不一定需要对ref参数进行赋值或修改,因此传入前必须具有初值; 函数内部必须对out参数进行赋值,因此传入前可能没有初值。

06 params: 可变数目的参数⚓︎

params 关键字可以指定采用数目可变的参数的方法参数。

可变数目参数规则

  1. 参数类型必须是一维数组;
  2. 在方法声明中只允许有一个 params 关键字;
  3. 在方法声明中的 params 关键字之后不允许有任何其他参数。

传参类型:

  1. 数组元素类型的参数的逗号分隔列表;
  2. 指定类型的参数的数组;
  3. 无参数。 如果未发送任何参数,则 params 列表的长度为零。

示例:

public class MyClass
{
    public static void UseParams(params int[] list)
    {
        for (int i = 0; i < list.Length; i++)
        {
            Console.Write(list[i] + " ");
        }
        Console.WriteLine();
    }

    public static void UseParams2(params object[] list)
    {
        for (int i = 0; i < list.Length; i++)
        {
            Console.Write(list[i] + " ");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        // You can send a comma-separated list of arguments of the
        // specified type.
        UseParams(1, 2, 3, 4);
        UseParams2(1, 'a', "test");

        // params参数接受零个或多个参数。  
        //下面的调用语句只显示空行。
        UseParams2();

        // An array argument can be passed, as long as the array
        // type matches the parameter type of the method being called.
        int[] myIntArray = { 5, 6, 7, 8, 9 };
        UseParams(myIntArray);

        object[] myObjArray = { 2, 'b', "test", "again" };
        UseParams2(myObjArray);

        // 编译异常,object数组不能转换为int数组
        // UseParams(myObjArray);

        // 下面的调用不会导致错误,而是整个int数组成为params数组的第一个元素。
        UseParams2(myIntArray);
    }
}
/*
Output:
1 2 3 4
1 a test

5 6 7 8 9
2 b test again
System.Int32[]
*/

五、函数重载与重写⚓︎

01 重写与重载⚓︎

  • 重写:对父类中继承的方法进行重写逻辑(详见[[06 抽象类与接口]])。
  • 重载:同名方法,不同功能。典型的重载:构造器重载、运算符重载。

02 函数重载⚓︎

函数重载规则:

  1. C#的函数重载函数名相同,参数数目或参数类型不同
  2. 不允许只有返回值不同的重载
  3. in out ref 不可作为重载标准,但带有参数修饰符与不带参数修饰符的方法可构成重载。
// 不允许重载
class CS0663_Example
{
    // Compiler error CS0663: "Cannot define overloaded
    // methods that differ only on in, ref and out".
    public void SampleMethod(in int i) { }
    public void SampleMethod(ref int i) { }
}

// 允许重载
class InOverloads
{
    public void SampleMethod(in int i) { }
    public void SampleMethod(int i) { }
}

Tip

当参数个数相同而参数类型不同的时候,可以考虑使用泛型,提高代码的复用性。

03 运算符重载⚓︎

C#允许使用用户定义的类型来重载运算符,运算符重载是为了自定义运算规则,比如复数的相加。

C#规定了可重载的运算符和不可重载的运算符,详见官方文档 运算符重载 | Microsoft Learn

使用 operator 关键字来声明需要重载的运算符,运算符重载必须符合以下规则:

  • 同时包含 public 和 static 修饰符。
  • 一元运算符有一个输入参数。 二元运算符有两个输入参数。 在每种情况下,都至少有一个参数必须具有类型 T 或 T?,其中 T 是包含运算符声明的类型。

示例:

public static Box operator +(Box b, Box c)
{
    Box box = new Box();
    box.length = b.length + c.length;
    box.breadth = b.breadth + c.breadth;
    box.height = b.height + c.height;
    return box;
}