>> >> >> Reference << << << <<<<<<Ref>>>>>>
OOP
Modified: 2025-06-01 | Author:ljf12825

C#是强类型,面向对象语言

类和对象

面向对象特性

C#中面向对象的实现

在C#中对象被视作引用类型存储在托管堆上

访问限定修饰符

默认访问修饰符

字段与属性

在C#中,字段和属性是用于存储数据的两种主要方式,它们之间有很大的区别

  1. 字段(Field) 字段是类或结构体中的一个变量,用于存储数据。它们通常直接定义在类中,并且可以通过类的实例进行访问 字段的特点:
  1. 属性(Property) 属性是C#中的一种特殊成员,用于对字段进行封装,是一种语法糖。它通常与字段关联,通过getter和setter方法来访问和修改字段的值

属性是C#中的一个重要特性,它不仅能提供对数据的访问,还可以在访问数据时添加额外的逻辑,比如验证、计算或其他操作。属性通常用于对类的字段提供更控制的访问方式

属性的特点:

自动实现的属性
从C# 3.0开始,如果不需要写特殊逻辑,可以用简化语法
自动实现的属性不需要显式定义字段,编译器会自动为其生成一个匿名的私有字段

class Person
{
    public string Name { get; set; } // 自动实现的属性
    public int Age { get; set; } // 自动实现的属性

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

手动实现的属性
手动实现的属性允许在访问和设置字段时添加自定义逻辑

class Person
{
    private int age; // 私有字段

    public int Age
    {
        get { return age; } // 获取age字段的值
        set
        {
            if (value >= 0) // 设置前验证
            {
                age = value;
            }
            else throw ArgumentOutOfRangeException("Age cannot be negative.");
        }
    }

    public Person(int age) => Age = age; // 通过get {} 访问
}

属性可以由不同的访问控制:

C# 9.0 提供了新的访问器init
只允许初始化时设置值 public string Name { get; init; }

public class Person
{
    public string Name { get; init; }
    public int Age { get; init; }
}

ver person = new Person
{
    Name = "Alice",
    Age = 30
};

// person.Name = "A" // 报错

抽象属性
抽象类可以拥有抽象属性,这些属性应该在派生类中被实现

public abstract class Person
{
    public abstract string Name { get; set; }
    public abstract int Age { get; set; }
}

class Student : Person
{
    public string Code { get; set; } = "N.A";
    public override string Name { get; set; } = "N.A";
    public override int Age { get; set; } = 0;
}

当创建该类的实例时,如果没有显式为Code,NameAge赋值,则默认值为给定值

表达式体属性
这种简化语法避免了使用完整的{ get; set; }

class Person
{
    private string name;

    public string Name
    {
        get => name; // 返回字段name的值
        set => name = value; // 设置字段name的值
    }
}

对于那些只包含单行逻辑的属性,使用=>语法可以使代码更加简洁,特别适合于只需要返回一个字段值的简单属性,或者只需要执行一个简单操作的set
只包含get的属性

class Person
{
    private string name = "N.A";

    public string Name => name; // get { return name; }
}

只包含set的属性

class Person
{
    private int age;

    public int Age
    {
        set => age = value;
    }
}

字段vs属性

特性字段属性
定义方式直接声明为类的成员变量通过 getset 方法定义并封装字段
访问控制通常通过访问修饰符来控制访问权限可以通过 getset 方法来控制访问权限
用途用于直接存储数据用于控制数据的访问,通常用于封装字段
数据验证不能直接进行数据验证可以在 set 访问器中实现数据验证逻辑
默认行为没有默认行为,直接存取数据允许在访问数据时加入逻辑或计算
继承行为字段不能在子类中访问,除非是 publicprotected属性可以被继承和重写,可以控制子类如何访问数据
性能直接存取,性能开销较小因为涉及方法调用,所以通常稍微慢一些,但差异通常很小

this

在C#中,this是一个关键字。它用于引用当前对象,尤其是在类的实例方法和构造函数中非常常见

  1. this用于引用当前实例 this关键字用于指向当前对象的实例。在类的成员方法或构造函数中,可以使用this来访问当前对象的字段、属性或方法
class Person
{
    public string Name;

    public void SetName(string name)
    {
        this.Name = name; // 使用this引用当前对象的字段
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person();
        person.SetName("Alice");
        Console.WriteLine(person.Name);
    }
}

SetName方法中,this.Name代表当前实例的Name字段,而方法参数name是传入的值。通过this.Name,明确区分了成员变量和参数变量

  1. this用于区分字段和参数 在类的方法中,当参数名称和成员字段名称相同时,可以使用this来区分它们,这对于避免变量命名冲突非常有用
class Person
{
    private string name;

    public Person(string name)
    {
        this.name = name; // 使用this来区分字段和参数
    }


    public void PrintName() => Console.WriteLine(this.Name); // 使用this引用字段
}

在构造函数中,this.name明确表示字段name,而构造函数的参数也叫name,使用this来区分这两个具有相同名称的成员

  1. this用于调用当前类的其他构造函数 C#允许类中使用this来调用当前类的其他构造函数,这叫做构造函数链。这种方式使得多个构造函数能够共享部分代码,避免重复
class Person
{
    public string Name;
    public int Age;

    // 默认构造函数
    public Person() : this("Unknown", 0) // 调用其他构造函数
    {}

    // 带参数的构造函数
    public Person(string name, int age)
    {
        this.Name = name;
        this.Age = age;
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person(); // 调用默认构造函数
        Console.WriteLine($"{person.Name}, {person.Age}"); // 输出"Unknown, 0"
    }
}
  1. this用于传递当前对象作为参数 有时可能需要将当前对象传递给其它方法或者类,这是this可以用来表示当前对象
class Person
{
    public string Name;

    public void PrintPerson()
    {
        Console.WriteLine(this.Name); // 输出当前对象的名称
    }

    public void PrintOtherPerson(Person otherPerson)
    {
        Consle.WriteLine(otherPerson.Name); // 输出其他Person对象的名称
    }

    public void CompareWithOtherPerson(Person otherPerson)
    {
        if (this == otherPerson) // 比较当前对象和传入对象
        {
            Console.WriteLine("The same Person");
        }
        else
        {
            Console.WriteLine("Different persons");
        }
    }
}

CompareWithOtherPerson方法中,this用来表示当前对象,便于和另一个对象进行比较

  1. this用于扩展方法 在C#中,扩展方法可以为现有类型添加方法,而无需修改它们的源代码。扩展方法的第一个参数是this,它表示要扩展的方法所在的对象类型
public static class PersonExtensions
{
    public static void PrintUpperCaseName(this Person person)
    {
        Console.WriteLine(person.Name.ToUpper()); // 使用this访问Person对象
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person { Name = "Alice" };
        person.PrintUpperCaseName(); // 输出“ALICE”(扩展方法)
    }
}

在这个例子中,this关键字使得PrintUpperCaseName方法能够扩展Person类型

  1. this与静态成员 this关键字不能在静态方法中使用,因为静态方法是属于类而不是类的实例的。在静态方法中,this没有意义
class Person
{
    public static void StaticMethod()
    {
        // this.Name = "Alice"; // 错误:无法在静态方法中使用'this'
    }
}

static

在C#中,static关键字用于定义静态成员,它使得某个成员属于类本身而不是某个类的某个实例

这意味着静态成员在所有类的实例中共享,而不是每个实例拥有一份独立的副本

static可以用于类、字段、方法、属性、构造函数等多个场景

静态类

static可以用来定义静态类,一个类如果声明为静态的,它就不能被实例化,也不能包含实例成员(如实例字段、实例方法等)静态类只能包含静态成员

public static class MathHelper
{
    public static double Pi = 3.14159;
    
    public static double Add(double a, double b) => a + b;
}

class Program
{
    static void Main()
    {
        // 访问静态类的成员
        Console.WriteLine(MathHelper.Pi); // 输出:3.14159
        Console.WriteLine(MathHelper.Add(2, 3)); // 输出:5
    }
}
静态字段

static可以用于声明静态字段。静态字段属于类本身,而不是类的某个实例。所有类的实例共享一个静态字段的值

public class Counter
{
    public static int Count = 0; // 静态字段

    public Counter() => Count++; // 每次创建一个实例时,静态字段Count会增加
}

class Program
{
    static void Main()
    {
        Console.WriteLine(Counter.Count); // 输出:0
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        Console.WriteLine(Counter.Count); // 输出:2
    }
}
静态方法

static也可以用于声明静态方法,静态方法只能访问静态成员,不能访问实例成员。静态方法通常用来执行类级别的操作,而不是操作某个具体实例的数据

public class MathHelper
{
    public static int Add(int a, int b) => return a + b;
}

class Program
{
    static void Main()
    {
        // 调用静态方法
        int result = MathHelper.Add(3, 4);
        Console.WriteLine(result); // 输出7
    }
}
静态构造函数

静态构造函数是一个特殊的构造函数,它用于初始化静态类成员,在类的任何静态成员第一次被访问之前,静态构造函数会自动被调用。静态构造函数没有访问修饰符,并且不能接收参数

public class MyClass
{
    public static int Counter;

    static MyClass()
    {
        Counter = 10; // 静态构造函数初始化静态成员
        Console.WriteLine("静态构造函数调用");
    }

    public static void PrintCounter() => Console.WriteLine(Counter);
}

class Program
{
    static void Main()
    {
        MyClass.PrintCounter(); // 输出10
    }
}
静态属性

static也可以用于声明静态属性,它允许你在类层面上控制静态字段的访问。静态属性是对静态字段的一种封装,通常用于获取或设置静态字段的值

public class GlobalSettings
{
    private static string settingValue;

    public static string Setting
    {
        get { return settingValue; }
        set { settingValue = value; }
    }
}

class Program
{
    static void Main()
    {
        GlobalSettings.Setting = "Dark Mode";
        Console.WriteLine(GlobalSettings.Setting); // 输出Dark Mode
    }
}

静态类的局限性

静态成员的使用场景

嵌套类

在C#中,嵌套类是指定义在另一个类内部的类。嵌套类可以时静态的或实例化的,并且它的访问权限可以受到外部类访问控制修饰符的限制。嵌套类通常用于以下几种情况:

嵌套类的定义

嵌套类通常定义在外部类内部,并且有自己的访问修饰符,这决定了它的可访问范围

public class OuterClass
{
    // 外部类的成员
    public int outerField;

    // 嵌套类
    public class NestedClass
    {
        public int nestedField;

        public void Display() => Console.WriteLine("Nested class method");
    }

    public void Display() => Console.WriteLine("Outer class method");
}

class Program
{
    static void Main()
    {
        // 创建外部类的实例
        OuterClass outer = new OuterClass();
        outer.outerField = 10;
        outer.Display();

        // 创建嵌套类的实例
        OuterClass.NestedClass nested = new OuterClass.NestedClass();
        nested.nestedField = 20;
        nested.Display();
    }
}
访问修饰符

嵌套类的访问修饰符决定了它的访问范围,嵌套类和它的成员可以设置为不同的访问级别

public class OuterClass
{
    private class PrivateNestedClass
    {
        public void Display()
        {
            Console.WriteLine("Private nested class");
        }
    }

    public class PublicNestedClass
    {
        public void Display()
        {
            Console.WriteLine("Public nested class");
        }
    }

    public void CreateNestedClasses()
    {
        // 只有外部类的实例方法能访问私有嵌套类
        PrivateNestedClass privateNested = new PrivateNestedClass();
        privateNested.Display();

        PublicNestedClass publicNested = new PublicNestedClass();
        publicNested.Display();
    }
}

class Program
{
    static void Main()
    {
        OuterClass outer = new OuterClass();
        outer.CreateNestedClasses();

        // 无法直接访问 PrivateNestedClass
        // OuterClass.PrivateNestedClass privateNested = new OuterClass.PrivateNestedClass(); // 错误!

        OuterClass.PublicNestedClass publicNested = new OuterClass.PublicNestedClass(); // 允许
        publicNested.Display();
    }
}
嵌套静态类

嵌套类可以是 静态的,这意味着它不能访问外部类的实例成员,只能访问外部类的静态成员。静态嵌套类通常用于封装一些与外部类实例无关的功能

public class OuterClass
{
    public static int staticField = 100;

    // 嵌套静态类
    public static class StaticNestedClass
    {
        public static void Display()
        {
            Console.WriteLine("Static nested class method");
            Console.WriteLine("Accessing static field from outer class: " + OuterClass.staticField);
        }
    }
}

class Program
{
    static void Main()
    {
        // 访问静态嵌套类
        OuterClass.StaticNestedClass.Display();
    }
}
嵌套类访问外部类的实例成员

嵌套类能够访问外部类的实例成员(字段和方法)。但要注意,嵌套类访问外部类实例成员时,必须先实例化外部类

public class OuterClass
{
    public int outerField = 42;

    // 嵌套类访问外部类实例成员
    public class NestedClass
    {
        public void Display(OuterClass outer)
        {
            Console.WriteLine("Outer class field: " + outer.outerField);  // 访问外部类的实例成员
        }
    }
}

class Program
{
    static void Main()
    {
        OuterClass outer = new OuterClass();
        OuterClass.NestedClass nested = new OuterClass.NestedClass();
        nested.Display(outer);  // 传入外部类实例
    }
}
嵌套类的使用场景
public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }

    // 嵌套类用于表示书籍的章节
    public class Chapter
    {
        public string Title { get; set; }
        public int PageCount { get; set; }

        public void Display() => Console.WriteLine($"{Title}, {PageCount}");
    }
}

class Program
{
    static void Main()
    {
        Book book = new Book { Title = "C# Programming", Author = "John Doe" };
        Book.Chapter chapter = new Book.Chapter { Title = "Introduction", PageCount = 10 };
        chapter.Display();
    }
}

扩展方法(Extension Methods)

在C#中,扩展方法是一种通过静态方法扩展现有类型功能的技术,而无需修改原始类型的代码。这使得可以为已经存在的类型(如系统库中的类)添加新方法,而不需要继承、修改类或重新编译库

定义
扩展方法实际上是静态方法,但是它们的调用方式和实例方法一样。扩展方法的定义依赖于this关键字,它指定了要扩展的类型

扩展方法的语法
扩展方法必须是静态的,并且定义在一个静态类中。扩展方法的第一个参数表示被扩展的类型,前面加上this关键字。通过这种方式,编译器会将扩展方法作为实例方法来调用

public static class ExtensionClass
{
    public static ReturnType MethodName(this TargetType instance, Parameters)
    {
        // 方法体
    }
}

示例\

  1. string类型扩展一个方法
    假设要为string类型扩展一个方法IsNullOrEmpty,用于检查字符串是否为null或空字符串
using System;

public static class StringExtensions
{
    // 扩展方法:检查字符串是否为null或空
    public static bool IsNullOrEmpty(this string str)
    {
        return string.InNullOrEmpty(str);
    }
}

class Program
{
    static void Main()
    {
        string text = "Hello, World!"
        bool result = text.IsNullOrEmpty(); // 调用扩展方法
        Console.WriteLine(result); // False

        string exptyText = "";
        Console.WriteLine(emptyText.IsNullOrEmpty()); //True
    }
}

在这个例子中,为string类型添加了一个IsNullOrEmpty方法,它检查字符串是否为空或为null

  1. List<int>扩展一个方法
    List<int>扩展一个Sum方法,用于计算一个整数列表的和
using System;
using System.Collections.Generic;

public static class ListExtensions
{
    // 扩展方法:计算整数列表的和
    public static int Sum(this List<int> list)
    {
        int sum = 0;
        foreach (var item in list) sum += item;
        
        return sum;
    }
}

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        Console.WriteLine(numbers.Sum());
    }
}
  1. DateTime扩展一个方法
    DateTime扩展一个方法,检查是否是工作日
using System;

public static class DateTimeExtensions
{
    // 扩展方法:检查日期是否是工作日
    public static bool IsWeekday(this DateTime date) => date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday;
}

注意事项\

实际应用\

特点
优点

缺点

抽象类与接口(interface)

抽象类和接口都是用来定义一组方法和属性,但它们的目的和使用方式不同

抽象类

抽象类是一种不能实例化的类,它提供一个模板,可以包含已实现的方法和未实现的方法(抽象方法)。抽象类允许部分实现,因此它可以定义字段、构造函数和方法,并且可以在其中包含抽象方法(没有方法体)
特点:

public abstract class Animal
{
    // 抽象方法:子类必须实现
    public abstract void MakeSound();

    // 已实现方法
    public void Sleep() => Console.WriteLine("Sleeping...");
}

public class Dog : Animal
{
    // 必须实现抽象方法
    public override void MakeSound() => Console.WriteLine("Bark!");
}

class Program
{
    static void Main()
    {
        // Animal animal = new Animal(); // 不能实例化抽象类
        Animal dog = new Dog(); // 通过子类实例化
        dog.MakeSound(); // Bark!
        dog.Sleep(); // Sleeping...
    }
}

Animal本身不能实例化,只能通过其子类来实例化

使用场景\

接口

接口是一种定义契约的方式,所有声明的成员都是抽象的(没有实现)。接口只能包含方法、属性、事件或索引器的声明,而不能包含任何实现。类可以实现多个接口,但它们必须实现接口中的所有方法

特点:

public interface IAnimal
{
    // 接口方法:没有实现
    void MakeSound();
}

public interface ICanFly
{
    void Fly();
}

public class Bird : IAnimal, ICanFly
{
    public void MakeSound()
    {
        Console.WriteLine("Chirp!");
    }

    public void Fly()
    {
        Console.WriteLine("Flying...");
    }
}

class Program
{
    static void Main()
    {
        IAnimal animal = new Bird();
        animal.MakeSound(); // 输出:Chirp

        ICanFly bird = new Bird();
        bird.Fly(); // 输出:Flying...
    }
}

使用场景

抽象类与接口
特性抽象类接口
设计理念is-acan-do
实现方法可以有部分实现(普通方法)只能有方法声明,无实现
构造函数可以有构造函数不能有构造函数
字段可以有字段(实例或静态)不能有字段
多继承只能继承一个类可以实现多个接口
实现方式类继承,子类必须实现所有抽象方法类实现,必须实现接口中的所有方法
成员访问控制可以有访问修饰符(publicprotectedprivate等)所有成员默认是 public,不能有访问修饰符
默认行为可以有默认实现(非抽象方法)所有方法默认是抽象的,不能有实现

继承问题

在C#中,继承方式总是public,接口只能继承接口,类只能继承一个类,但可以继承多个接口,以实现多继承,同时避免了菱形继承问题,因为接口中只有函数签名,实现在子类中‘

virtualabstract
  1. virtual方法

virtual关键字用于允许类中的方法、属性和事件被重写(override)或继承,但它不强制要求子类必须重写该方法。如果子类没有重写该方法,它将调用基类提供的默认实现

特点:

public class Animal
{
    // virtual方法,允许被重写
    public virtual void Speak() => Console.WriteLine("Animal speaks");

    public class Dog : Animal
    {
        // 重写基类的虚拟方法
        public override void Speak() => Console.WriteLine("Dog barks");
    }
}

public class Program
{
    public static void Main()
    {
        Animal myAnimal = new Animal();
        myAnimal.Speak(); // Animal speaks

        Animal myDog = new Dog();
        myDog.Speak(); // Dog barks
    }
}

使用场景

  1. abstract方法 abstract关键字用于声明抽象方法,这意味着该方法在基类中没有实现,并且必须在派生类中实现。抽象方法只能存在于抽象类中,抽象类时是不能被实例化的,它用于定义子类的模板

特点:

public abstract class Animal
{
    // abstract方法,必须在派生类中实现
    public abstract void Speak();
}


public class Dog : Animal
{
    // 必须实现基类的抽象方法
    public override void Speak() => Console.WriteLine("Dog barks");
}

public class Program
{
    public static void Main()
    {
        // Animal myAnimal = new Animal(); // 错误:不能实例化抽象类
        Animal myDog = new Dog();
        myDog.Speak(); // 输出:Dog barks
    }
}

使用场景

virtual vs abstract

特性virtual 方法abstract 方法
方法实现有默认实现,可以选择重写没有默认实现,必须在派生类中实现
派生类的要求派生类可以选择是否重写方法派生类必须实现该方法
可用范围可以用于普通类,派生类可以选择是否重写只能用于抽象类,必须被重写
是否可以实例化可以在基类中实例化不能实例化抽象类
行为允许派生类重写该方法强制派生类提供该方法的实现

virtualabstract的组合使用 可以在某些情况下结合使用virtualabstract,例如,在抽象类中定义一个抽象方法,然后提供一个虚拟方法作为默认实现,这样派生类可以选择重写该方法,或者使用基类提供的默认实现

public abstract class Animal
{
    // abstract 方法:强制子类实现
    public abstract void MakeSound();

    // virtual 方法:提供默认实现,可以选择重写
    public virtual void Sleep() => Console.WriteLine("Aniaml sleeps");
}

public class Dog : Animal
{
    public override void MakeSound() => Console.WriteLine("Dog barks");

    // 可以选择重写虚拟方法
    public override void Sleep() => Console.WriteLine("Dog sleeps");
}

public class Program
{
    public static void Main()
    {
        Animal myDog = new Dog();
        myDog.MakeSound(); // Dog barks
        myDog.Sleep(); // Dog sleeps
    }
}

运算符重载

构造函数和析构函数

在C#中,构造函数和析构函数时特殊的方法,它们分别在创建对象和销毁对象时被自动调用。它们用于初始化对象和清理资源

构造函数(Constructor)

构造函数时当创建对象时自动调用的特殊方法,它用于初始化对象,通常用于设置对象的初始状态

特点:

构造函数的类型

public class MyClass
{
    public static int StaticValue;

    // 静态构造函数
    static MyClass()
    {
        Console.WriteLine("Static constructor called.");
        StaticValue = 10;
    }

    public static void DisplayValue() => Console.WriteLine($"Static Value: {StaticValue}");
}

class Program
{
    static void Main()
    {
        MyClass.DisplayValue(); 
    }
}
析构函数(Destructor)

析构函数时当对象销毁时自动调用的特殊方法,主要用于释放资源,尤其时非托管资源(如文件句柄、数据库连接等)

在C#中,析构函数通常用来清理那些没有被垃圾回收器管理的资源

特点:

注意事项:

partial

partial关键字的作用是将一个类、结构、接口、方法的定义拆分到多个文件或多个位置,在编译时由C#编译器把它们合并成一个完整定义

它本质上就是编译器的代码拼接,运行时你看到的还是一个完整的类型或方法

示例
  1. 拆分类
// Player.Data.cs
public partial class Player
{
    public string Name;
    public int Level;
}
// Player.Logic.cs
public partial class Player
{
    public void Levelup()
    {
        Level++;
        Console.WriteLine($"{name} 升级到 {Level}级!");
    }
}

编译时,C#会自动把Player.Data.csPlayer.Logic.cs的代码合并成一个Player

  1. 拆分方法 C# 3.0引入了partial method,用于让一个方法的声明和实现分开,且实现可选
// Game.partial.cs
public partial class Game
{
    partial void OnGameStart(); // 声明(没有方法体)
}
// Game.impl.cs
public partial class Game
{
    partial void OnGameStart()// 实现
    {
        Console.WriteLine("游戏开始!");
    }
}

特点:

常见应用场景
  1. 代码生成 + 手写代码分离 比如WinForms、WPF、Unity自动生成的UI代码
// Form1.Designer.cs(自动生成)
partial class Form1
{
    private void InitializeComponent() { ... }
}
// Form1.cs (手写逻辑)
partial class Form1
{
    private void Form1_Load(...) { ... }
}

这样自动生成的代码和手写的逻辑分开,避免每次UI改动都覆盖手写部分

  1. 大型类文件拆分 当一个类太大时,可以按功能模块拆到多个文件
Enemy.Movement.cs
Enemy.Attack.cs
Enemy.Health.cs

阅读和维护更轻松

  1. 团队协作 多人维护同一个类时,partial允许每个人在不同文件里写各自负责的部分,减少合并冲突
注意事项
  1. 所有partial部分必须
  1. 编译时自动合并,不会影响运行时性能

  2. partial method如果没有实现,编译器会把它和调用都优化掉

readonly

在C#中,readonly关键字用于修饰字段,使得这些字段在初始化后不能再修改,确保字段在对象生命周期内的不可变性。具体来说,readonly适用于以下情况

  1. 只能在构造函数中初始化 readonly字段只能在构造函数中被复制,或者在声明时进行初始化。它不能在类的其他方法或外部代码中被修改
public class MyClass
{
    public readonly int x;
    
    public MyClass(int val) => x = val; // 可以在构造函数中赋值
}
  1. 不可再其他方法或外部修改 一旦被赋值,readonly字段的值就不能再被修改。如果尝试再其他地方修改它,编译器会报错
public class MyClass
{
    public readonly int x = 10;

    public void ChangeValue()
    {
        // x = 20; // 编译错误,无法修改只读字段
    }
}
  1. 对于静态字段 如果一个字段被声明为static readonly,它将是类级别的常量,并且只能再静态构造函数中初始化
public class MyClass
{
    public staitc readonly string MyConstant = "Hello, World!";

    static MyClass()
    {
        // 也可以再静态构造函数中初始化
    }
}
  1. const的区别
public class MyClass
{
    public const int ConstantValue = 100; // 编译时确定
    public readonly int ReadonlyValue; // 运行时可以初始化

    public MyClass(int value) => ReadonlyValue = value;
}
常见用法

readonly主要用于哪些需要保证值在对象生命周期内不变的场景,比如配置参数、唯一标识符等 例如,一个表示”出生日期”的字段可以使用readonly

public class Person
{
    public readonly DateTime DateOfBirth;

    public Person(DateTime birthDate) => DateOfBirth = birthDate;
}

这样一来,DateOfBirth一旦初始化后,就不能在对象的生命周期中被改变,确保了数据的安全性和一致性

sealed

sealed(密封)是一个关键字,用于限制类或类成员的继承和重写,控制继承行为,并非访问修饰符

sealed修饰类

当一个类被声明为sealed时,它不能被其他类继承

sealed class SealedClass
{
    public void Display() => Console.WriteLine("这是一个sealed类");
}

// class DerivedClass : SealedClass { } // Error: 'DerivedClass': cannot derive from sealed type 'SealedClass'

使用场景

sealed修饰方法

当方法被声明为sealed时,它不能在派生类中被重写。注意:sealed只能用于已标记为override的方法

class BaseClass
{
    public virtual void Method1() => Console.WriteLine("Base Method1");

    public virtual void Method2() => Console.WriteLine("Base Method2");
}

class DerivedClass : BaseClass
{
    // 重写并密封 Method1,防止进一步重写
    public sealed override void Method1() => Console.WriteLine("Derived sealed Method1");

    // 只重写Method2,没有密封
    public override void Method2() => Console.WriteLine("Derived Method2");
}

class ThirdClass : DerivedClass
{
    // public override void Method1() { } // 错误,不能重写sealed方法

    public override void Method2() { } // 可以重写非sealed方法
}
破坏sealed

在正常的C#编程中,没有官方支持的方法可以打破sealed的限制
但有一些非正统的技术手段,不过这些都有严重的问题,不推荐在生产环境中使用

  1. 反射(有限度的打破) 只能访问成员,不能继承sealed类

  2. 直接使用指针操作内存(危险行为)

  3. 使用DynamicMethod或IL生成 可以绕过一些编译时检查,但极其复杂且不稳定

  4. 序列化技巧 通过序列化/反序列化可能改变对象状态,但无法改变类的继承关系

使用sealed的好处
  1. 安全性:防止关键功能被修改
  2. 性能:编译器可以进行更好的优化
  3. 设计意图明确:明确表示该类或方法不应被进一步扩展
  4. 版本控制:确保基类行为的一致性
注意事项

base

base关键字用于从派生类(子类)中访问基类(父类)的成员。它的作用类似于对象自身的this关键字,但base指向的是其直接基类

主要用途
  1. 调用基类中已被重写的方法 当在派生类中重写(使用override关键字)了也给基类的虚方法或抽象方法后,有时希望在执行新代码的同时,仍然保留基类方法的原有逻辑。这时候就可以使用base来调用它
public class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("The animal makes a sound.");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        // 调用基类 Animal 的 MakeSound 方法
        base.MakeSound(); // 输出:The animal makes a sound.

        // 然后添加狗特有的行为
        Console.WriteLine("The dog barks: Woof woof!");
    }
}

class Program
{
    static void Main()
    {
        Dog myDog = new Dog();
        myDog.MakeSound();
        // 输出:
        // The animal makes a sound.
        // The dog barks: Woof woof!
    }
}

在这个例子中,Dog类重写了MakeSound方法。通过base.MakeSound(),它首先执行了基类Animal中的通用逻辑,然后才执行自己特有的逻辑
它避免了代码重复,可以在扩展功能的同时,复用基类已经实现好的、稳定的核心功能

  1. 调用基类的构造函数(特别是在派生类的构造函数中) 这是base关键字非常常见和重要的用法。当一个派生类被实例化时,其构造函数首先调用其基类的构造函数,以确保基类部分的成员被正确初始化。这个过程是自动的,但有时需要显式地控制调用基类的哪一个构造函数
public class Vehicle
{
    public string Brand { get; set; }
    public int Year { get; set; }

    // 基类构造函数 1
    public Vehicle(string brand)
    {
        Brand = brand;
        Console.WriteLine($"Vehicle constructor called for {brand}");
    }

    // 基类构造函数 2
    public Vehicle(string brand, int year) : this(brand) // 使用 this 调用同一个类的另一个构造函数
    {
        Year = year;
        Console.WriteLine($"Vehicle constructor with year called. Year: {year}");
    }
}

public class Car : Vehicle
{
    public int NumberOfDoors { get; set; }

    // Car 的构造函数,调用基类 Vehicle 的构造函数 (string, int)
    public Car(string brand, int year, int doors) : base(brand, year) // 关键在这里!
    {
        NumberOfDoors = doors;
        Console.WriteLine($"Car constructor called. Doors: {doors}");
    }
}

class Program
{
    static void Main()
    {
        Car myCar = new Car("Toyota", 2023, 4);
    }
}

// 输出
// Vehicle constructor called for Toyota
// Vehicle constructor with year called. Year: 2023
// Car constructor called. Doors: 4

执行顺序

  1. 创建Car对象时,首先进入Car的构造函数
  2. 但由于有: base(brand, year),所以程序会先跳转到基类Vehicle匹配的构造函数Vehicle(string brand, int year)
  3. Vehicle(string brand, int year)中,又因为: this(brand),会先调用Vehicle(string brand)
  4. 执行完所有基类构造函数的逻辑后,最后才回到Car的构造函数体中执行NumberOfDoors = doors;
重要规则