类与接口
类和接口是面向对象编程(OOP)的两个核心概念。它们提供了不同的抽象方式来定义对象的行为和结构。
访问修饰符
在 C# 编程中,访问修饰符用于控制类、结构、接口、方法和属性等成员的可见性和访问级别。正确使用访问修饰符可以增强代码的封装性、安全性和可维护性。
访问修饰符 | 类内部 | 同一类的派生类 | 同一命名空间 | 同一程序集 | 不同命名空间和程序集 |
---|---|---|---|---|---|
public | 是 | 是 | 是 | 是 | 是 |
protected internal | 是 | 是 | 是 | 是 | 否 |
internal | 是 | 否 | 是 | 是 | 否 |
protected | 是 | 是 | 否 | 否 | 否 |
private protected | 是 | 是(同一程序集) | 否 | 否 | 否 |
private | 是 | 否 | 否 | 否 | 否 |
类
类是C#中用于定义对象的蓝图或模板。它可以包含数据和行为。类是C#中实现对象的基本单元。
特点:
- 封装(Encapsulation):类将数据和方法封装在一起,通过属性和方法对外公开。
- 继承(Inheritance):一个类可以继承另一个类,从而重用代码。C#只支持单继承,但可以通过接口实现多重继承的效果。
- 多态(Polymorphism):类可以通过虚方法和重写实现多态,允许子类修改父类的行为。
- 抽象(Abstraction):类可以通过抽象类和方法来实现部分抽象。
成员 | 描述 |
---|---|
字段 | 字段是在类范围声明的变量。 字段可以是内置数值类型或其他类的实例。 例如,日历类可能具有一个包含当前日期的字段。 |
常量 | 常量是在编译时设置其值并且不能更改其值的字段。 |
属性 | 属性是类中可以像类中的字段一样访问的方法。 属性可以为类字段提供保护,以避免字段在对象不知道的情况下被更改。 |
方法 | 方法定义类可以执行的操作。 方法可接受提供输入数据的参数,并可通过参数返回输出数据。 方法还可以不使用参数而直接返回值。 |
事件 | 事件向其他对象提供有关发生的事情(如单击按钮或成功完成某个方法)的通知。 事件是使用委托定义和触发的。 |
运算符 | 重载运算符被视为类型成员。 重载运算符时,将其定义为类型中的公共静态方法。 有关详细信息,请参阅运算符重载。 |
索引器 | 使用索引器可以用类似于数组的方式为对象建立索引。 |
构造函数 | 构造函数是首次创建对象时调用的方法。 它们通常用于初始化对象的数据。 |
终结器 | C# 中很少使用终结器。 终结器是当对象即将从内存中移除时由运行时执行引擎调用的方法。 它们通常用来确保任何必须释放的资源都得到适当的处理。 |
嵌套类型 | 嵌套类型是在其他类型中声明的类型。 嵌套类型通常用于描述仅由包含它们的类型使用的对象。 |
成员
常量
常量是不可变的值,在编译时是已知的,在程序的生命周期内不会改变。
常量使用 const 修饰符声明。
常量的特点:
- 必须初始化:在声明常量时,必须立即对其赋值。
- 编译时确定:常量的值在编译时确定,无法在运行时更改。
- 隐式静态:常量是隐式静态的,不需要使用 static 关键字。
- 只能使用简单类型:常量只能使用内置的基本数据类型(如 int、double、char 等),或者 string 类型。用户定义的类型(包括类、结构和数组)不能为 const。 使用 readonly 修饰符创建在运行时一次性(例如在构造函数中)初始化的类、结构或数组,此后不能更改。
- C# 不支持 const 方法、属性或事件。
示例:
public class MathConstants
{
public const double Pi = 3.14159;
public const int MaxValue = 100;
}
//访问
Console.WriteLine(MathConstants.Pi); // 输出: 3.14159
常量与 readonly 的区别
const:值在编译时确定,不能被修改,适用于编译时已知的值。
readonly:值可以在运行时确定,但只能在构造函数中赋值,之后不可修改。
特性 | const | readonly |
---|---|---|
初始化时间 | 编译时 | 运行时(可以在构造函数中初始化) |
值是否可变 | 否 | 只可在构造函数中赋值,之后不可修改 |
数据类型支持 | 基本数据类型和 string | 任何类型 |
是否隐式静态 | 是 | 否(但可以显式声明为静态) |
readonly 示例:
public class Config
{
public readonly int MaxConnections;
public Config(int maxConnections)
{
MaxConnections = maxConnections;
}
}
字段和属性
在C#中,字段和属性是类或对象的数据成员,但它们的使用方式和目的有所不同。
字段是类或结构中直接存储数据的成员,通常用于存储对象的内部状态。
属性是对类中的字段提供访问的方法。它们提供了对字段的间接访问,允许控制对字段的读取和写入操作。通过属性,可以在获取或设置值时添加逻辑,比如验证或修改输入。
特性 | 字段(Field) | 属性(Property) |
---|---|---|
访问方式 | 直接访问 | 通过 get 和 set 访问器间接访问 |
数据封装 | 通常是私有的,直接存储数据 | 提供对字段的控制访问,支持额外逻辑处理 |
访问控制 | 通常只能控制访问修饰符 | 可以通过 get 和 set 访问器控制读取与写入 |
自动实现 | 需要手动声明并管理 | 支持自动属性,由编译器自动生成字段 |
复杂逻辑 | 不适合直接放置验证或计算逻辑 | 可以在 set 或 get 中添加逻辑 |
字段
字段是类或结构中直接存储数据的成员,通常用于存储对象的内部状态。
特点:
- 字段通常是私有的 (private),避免外部类直接访问它们。
- 字段可以是任何类型,包括值类型和引用类型。
- 可以通过构造函数或直接赋值来初始化字段。
示例:
public class Person
{
private string name;
private int age;
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public void DisplayInfo()
{
Console.WriteLine($"Name: {name}, Age: {age}");
}
}
属性
属性是对类中的字段提供访问的方法。它们提供了对字段的间接访问,允许控制对字段的读取和写入操作。通过属性,可以在获取或设置值时添加逻辑,比如验证或修改输入。
- 属性使用 get 和 set 访问器来控制对字段的访问。
- get 访问器用于返回属性的值,set 访问器用于赋值。
- 属性可以是只读的(只有 get 访问器)或可读写的(有 get 和 set 访问器)。
- 属性可以通过自定义逻辑来控制赋值或获取值的方式。
public class Person
{
private string name;
private int age;
public string Name
{
get { return name; }
set { name = value; }
}
public int Age
{
get { return age; }
set
{
if (value >= 0)
age = value;
}
}
public void DisplayInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
自动属性(Auto-Implemented Properties)
自动属性提供了一种简化属性声明的方式,C# 编译器会自动为它们创建一个私有的匿名字段。在不需要添加额外逻辑时,可以使用自动属性。
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void DisplayInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
required
在C#中,required 字段是从C# 11开始引入的一个新特性,允许你标记一个字段或属性为必需,即在对象初始化时必须赋值。
这个功能非常适合用于数据模型或POCO(Plain Old CLR Object)类,以确保在对象实例化时,所有必需的字段都得到了正确的初始化。
示例:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void DisplayInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
required 与构造函数的对比
- 使用 required 可以避免为每个类都创建多个不同参数的构造函数,简化代码编写和维护。
- 在对象初始化时,required 字段的赋值更加灵活,结合对象初始化器(Object Initializer)使代码更具可读性。
注意事项
- required 属性和字段的初始化顺序依赖于对象初始化器。
- required 字段仅在类中可用,不适用于结构体。
- 当 required 字段或属性在继承的类中时,派生类也必须初始化这些字段或属性。
方法
参考 方法
事件
参考 委托与事件
运算符
参考 运算符与表达式
索引器
索引器是C#中的一种特殊属性,允许对象像数组一样通过索引来访问内部元素。通过索引器,可以使用类似数组的语法来访问类或结构体中的集合,而无需显式调用方法。
索引器的作用是通过提供的索引来访问对象的集合元素(如数组、列表或字典等)。在定义索引器时,可以自定义访问的逻辑,因此它非常灵活。
示例:
public class SampleCollection<T>
{
private T[] arr = new T[100]; // 私有数组用于存储元素
// 定义索引器
public T this[int i]
{
get { return arr[i]; } // 返回指定索引的元素
set { arr[i] = value; } // 设置指定索引的元素
}
}
//使用
SampleCollection<string> stringCollection = new SampleCollection<string>();
stringCollection[0] = "Hello";
stringCollection[1] = "World";
Console.WriteLine(stringCollection[0]); // 输出: Hello
Console.WriteLine(stringCollection[1]); // 输出: World
多索引示例:
public class Matrix
{
private int[,] matrix = new int[3, 3];
// 多维索引器
public int this[int row, int col]
{
get { return matrix[row, col]; }
set { matrix[row, col] = value; }
}
}
//使用
Matrix matrix = new Matrix();
matrix[0, 0] = 1;
matrix[0, 1] = 2;
Console.WriteLine(matrix[0, 0]); // 输出: 1
Console.WriteLine(matrix[0, 1]); // 输出: 2
特点:
- 类似数组的语法:可以通过索引的方式访问和设置数据,而不需要调用方法。
- 可以自定义逻辑:在 get 和 set 访问器中可以添加自定义的访问逻辑,如验证或转换。
- 支持多个索引:索引器可以接受多个参数,这类似于多维数组。
应用场景
- 自定义集合类:通过索引器,可以使自定义的集合类(如列表、矩阵等)像数组一样操作。
- 字典或映射:在一些映射结构中,可以通过索引器轻松实现键值对的访问。
构造函数
C# 构造方法(Constructor)用于在创建对象时初始化对象的状态。
构造方法与类同名,不需要指定返回类型。在 C# 中,构造方法可以被重载,以提供不同的初始化方式。
默认构造方法
如果未显式定义构造方法,编译器会提供一个默认的无参数构造方法。
public class Person
{
// 默认构造方法,由编译器提供
}
无参数构造方法
你也可以手动定义无参数构造方法,用于自定义初始化。
public class Person
{
public string Name;
public Person()
{
Name = "未知";
}
}
带参数构造方法
允许在创建对象时传递参数,以初始化成员变量。
public class Person
{
public string Name;
public int Age;
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
静态构造方法
用于初始化静态成员或执行仅需一次的操作。
静态构造方法在第一次创建实例或引用任何静态成员时被调用,且只能有一个,不能带访问修饰符和参数。
public class Config
{
public static readonly string ConnectionString;
static Config()
{
ConnectionString = "Data Source=...";
}
}
构造方法的重载
C#允许同一个类中定义多个构造方法,只要它们的参数列表不同。这种机制称为构造方法重载。
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
// 无参构造方法
public Person()
{
Name = "Unknown";
Age = 0;
}
// 有参构造方法
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
嵌套类型(内部类型)
在 C# 中,嵌套类型指的是在一个类、结构体或接口内部定义的类型。
嵌套类型通常用于表示外部类型的某种内部概念,或者仅供外部类型使用的辅助类型。
嵌套类型可以是类、结构体、接口、枚举或委托。
特性;
- 不论外部类型是类、接口还是构造,嵌套类型均默认为 private;仅可从其包含类型中进行访问。
- “类”的嵌套类型可以是 public、protected、internal、protected internal、private 或 private protected。
- 在密封类中定义 protected、protected internal 或 private protected 嵌套类将产生编译器警告 CS0628“封闭类汇中声明了新的受保护成员”。
- 使嵌套类型在外部可见违反了代码质量规则 CA1034“嵌套类型不应是可见的”。
- 构造的嵌套类型可以是 public、internal 或 private。
- 嵌套类型(或内部类型)可访问包含类型(或外部类型)。 若要访问包含类型,请将其作为参数传递给嵌套类型的构造函数。
嵌套类
嵌套类是定义在另一个类内部的类。它可以访问外部类的成员(包括私有成员),并且通常用于封装外部类中的逻辑或提供一些内部服务。
public class OuterClass
{
private int outerValue = 100;
// 嵌套类
public class NestedClass
{
public void Display(OuterClass outer)
{
// 嵌套类可以访问外部类的私有成员
Console.WriteLine(outer.outerValue);
}
}
}
//调用
OuterClass outer = new OuterClass();
OuterClass.NestedClass nested = new OuterClass.NestedClass();
nested.Display(outer);
嵌套结构体
你可以在类或其他结构体中定义结构体,这种嵌套类型结构体适合用于封装轻量级的值类型逻辑。
public class Container
{
public struct Dimensions
{
public int Width;
public int Height;
public Dimensions(int width, int height)
{
Width = width;
Height = height;
}
}
}
Container.Dimensions dim = new Container.Dimensions(100, 200);
Console.WriteLine($"Width: {dim.Width}, Height: {dim.Height}");
嵌套接口
接口也可以嵌套在类或结构体中。嵌套接口通常用于定义外部类或结构体的内部行为。
public class Engine
{
// 嵌套接口
public interface IStartable
{
void Start();
}
}
public class Car : Engine.IStartable
{
public void Start()
{
Console.WriteLine("Car started.");
}
}
Car car = new Car();
car.Start();
嵌套枚举
枚举可以嵌套在类、结构体或接口中,表示外部类型的一组内部状态或常量值。
public class Player
{
// 嵌套枚举
public enum PlayerState
{
Idle,
Running,
Jumping
}
public PlayerState State { get; set; }
}
Player player = new Player();
player.State = Player.PlayerState.Running;
Console.WriteLine(player.State);
表达式主体成员
表达式主体成员(Expression-bodied members)是C#中一种简化语法,用来定义简单的成员(如方法、属性、构造函数等)。
它通过 =>(lambda 运算符)来返回值或执行操作,适合于单行的逻辑处理,简化代码的书写和提高可读性。
表达式主体方法
表达式主体方法适用于返回值或包含简单的单行逻辑的函数。
public class Calculator
{
// 使用表达式主体定义一个方法
public int Add(int a, int b) => a + b;
}
表达式主体属性
表达式主体属性可以用于简化只读属性的定义,尤其是当 get 访问器只有一行时。
public class Person
{
private string name;
// 使用表达式主体定义只读属性
public string Name => name;
}
表达式主体的 set 和 get 访问器
从C# 7.0开始,属性的 get 和 set 访问器也可以用表达式主体来定义。
示例:
public class Person
{
private string name;
public string Name
{
get => name; // 表达式主体的 get 访问器
set => name = value; // 表达式主体的 set 访问器
}
}
表达式主体构造函数和析构函数
构造函数和析构函数也可以使用表达式主体的语法来简化实现。
public class Person
{
private string name;
// 构造函数使用表达式主体
public Person(string name) => this.name = name;
// 析构函数
~Person() => Console.WriteLine("Destructor called");
}
表达式主体索引器
索引器也可以使用表达式主体语法。
public class Collection
{
private string[] data = { "A", "B", "C" };
// 索引器使用表达式主体
public string this[int index] => data[index];
}
表达式主体事件
从 C# 7.0 开始,事件的 add 和 remove 访问器也可以用表达式主体来定义。
public class EventExample
{
private event EventHandler myEvent;
public event EventHandler MyEvent
{
add => myEvent += value; // 使用表达式主体
remove => myEvent -= value; // 使用表达式主体
}
}
加载顺序
静态构造方法(子类) => 静态构造方法(父类)=> 实例构造方法(父类)=> 实例构造方法(子类)。
public class Parent
{
static Parent()
{
Console.WriteLine("父类静态构造方法被调用");
}
public Parent()
{
Console.WriteLine("父类实例构造方法被调用");
}
}
public class Child : Parent
{
static Child()
{
Console.WriteLine("子类静态构造方法被调用");
}
public Child() : base()
{
Console.WriteLine("子类实例构造方法被调用");
}
}
class Program
{
static void Main()
{
Child child = new Child(); // 创建子类实例
}
}
//输出:
// 子类静态构造方法被调用
// 父类静态构造方法被调用
// 父类实例构造方法被调用
// 子类实例构造方法被调用
抽象类(abstract)
在 C# 中,抽象类(abstract class)是一种不能被直接实例化的类。
它可以包含抽象方法、普通方法、属性、字段和构造方法。
抽象类的主要目的是为派生类提供一个基本的框架,强制子类实现某些方法。
示例:
public abstract class Animal
{
protected string name;
//构造方法
protected Animal(string name)
{
this.name = name;
}
// 抽象方法,没有实现
public abstract void MakeSound();
// 虚方法,默认实现
public virtual void Move()
{
Console.WriteLine(name + "Animal Move...");
}
// 具体方法,有实现
public void Eat()
{
Console.WriteLine(name + "Animal Eating...");
}
}
public class Dog : Animal
{
// 构造方法
public Dog(string name): base(name)
{
this.name = name;
}
// 实现抽象方法(必须)
public override void MakeSound()
{
Console.WriteLine(name + "Dog MakeSound!");
}
// 不重写会有调用方法
public override void Move()
{
Console.WriteLine(name + "Dog Move...");
base.Move();
}
}
class Program
{
static void Main()
{
Dog dog = new Dog("张三 ");
dog.MakeSound(); // 张三Dog MakeSound!
dog.Move(); // 张三Dog Move... 张三Animal Move...
dog.Eat(); // 张三Animal Eating...
//Animal animal = new Animal("张三 "); 编译错误
}
}
抽象类与接口的比较
抽象类:
- 可以包含字段、构造方法、属性和具体方法。
- 支持访问修饰符(如 private, protected, public)。
- 只能继承一个抽象类(单继承)。
接口:
- 只能包含方法签名、属性、事件和索引器,不可以有字段和实现。
- 方法默认是 public,接口中的成员不能有访问修饰符。
- 支持多重继承(一个类可以实现多个接口)。
抽象类的应用场景
- 定义模板:为子类提供通用的基类和基本实现。
- 强制实现:强制子类实现某些方法,从而遵循一定的契约。
- 封装共享代码:将共有的实现和数据封装在抽象类中,提高代码复用性。
abstract 方法 和 virtual 方法
abstract 方法:
- 是一种没有实现的纯虚方法,必须在派生类中实现。
- 只能在抽象类(abstract class)中定义。
- 不提供任何默认行为,必须由派生类进行实现。
virtual 方法:
- 是一种具有默认实现的方法,派生类可以选择是否重写它。
- 可以在任何非密封类中定义。
- 提供一个默认行为,但允许子类通过重写来修改或扩展该行为。
密封类(sealed)
在 C# 中,sealed 关键字用于防止类被继承,也可以用于防止虚方法被进一步重写。
密封类和密封方法都具有强制性的特点,目的是确保类或方法不再允许进一步扩展或修改。
密封类(Sealed Classes)
当一个类被声明为 密封类 时,其他类不能继承它。
这通常用于设计不能被扩展的类,确保类的实现不会被修改,特别是在安全性或完整性很重要的场合。
示例:
public sealed class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
// 错误示例
public class AdvancedCalculator : Calculator
{
// 编译错误: 'Calculator' 类是密封的,不能被继承
}
密封方法(Sealed Methods)
示例:
public class BaseClass
{
// 定义虚方法,允许子类重写
public virtual void Display()
{
Console.WriteLine("BaseClass Display");
}
}
public class DerivedClass : BaseClass
{
// 重写基类方法
public override void Display()
{
Console.WriteLine("DerivedClass Display");
}
}
public class MoreDerivedClass : DerivedClass
{
// 重写并密封方法,防止进一步重写
public sealed override void Display()
{
Console.WriteLine("MoreDerivedClass Display");
}
}
// 错误示例
public class MostDerivedClass : MoreDerivedClass
{
public override void Display()
{
// 编译错误: 无法重写密封方法 'MoreDerivedClass.Display'
}
}
密封类的场景
- 安全性要求:类的实现涉及到安全性要求,或者其行为不能被修改。
- 性能优化:密封类可以在某些情况下提高运行时性能,因为编译器可以对其做出更多优化。
- 设计封闭:设计上不希望其他类继承或扩展该类的功能。
密封方法的场景
- 方法实现完整性:当基类中某些方法的重写在特定的派生类中已经达到期望效果,且不希望再被进一步修改时,可以密封它。
- 代码设计控制:控制某些核心逻辑或算法不被其他派生类篡改。
静态类(static)
在 C# 中,静态类(static class)是一种特殊的类,它的特点是只能包含静态成员。
静态类不能被实例化,也不能被继承。
这种类通常用于组织工具方法、全局状态或者不依赖于实例的功能。
特点:
- 只能包含静态成员:静态类的所有成员必须是静态的,包括字段、属性、方法、事件等。不能包含实例成员。
- 不能实例化:不能通过 new 关键字创建静态类的对象。
- 不能继承或被继承:静态类不能继承自其他类,也不能被继承。
- 隐式密封:静态类在设计上是密封的,即无法继承其他类或者被其他类继承。
- 效率高:由于静态类不需要实例化,直接通过类名访问,因此效率较高。常用于提供不依赖于对象实例的全局功能。
public static class MathUtilities
{
// 静态构造函数
// 在第一次使用类时自动调用。静态构造函数不能带参数,且只能访问静态成员。
static MathUtilities()
{
Console.WriteLine("Static constructor called.");
}
// 静态字段
public static double Pi = 3.14159;
// 静态方法
public static double Square(double number)
{
return number * number;
}
}
public class Program
{
static void Main(string[] args)
{
// 直接通过类名访问静态成员
double result = MathUtilities.Square(5);
Console.WriteLine($"Square of 5 is: {result}"); // 输出: Square of 5 is: 25
// 访问静态字段
Console.WriteLine($"Value of Pi is: {MathUtilities.Pi}"); // 输出: Value of Pi is: 3.14159
}
}
静态类 vs 普通类
特性 | 静态类 | 普通类 |
---|---|---|
是否可以实例化 | 不能实例化 | 可以实例化 |
是否可以继承 | 不能继承或被继承 | 可以继承和被继承 |
成员类型 | 只能包含静态成员 | 可以包含静态和实例成员 |
访问方式 | 通过类名访问静态成员 | 通过实例访问实例成员,类名访问静态成员 |
主要用途 | 提供全局工具方法或全局状态 | 通常用于封装数据和行为的实例 |
分部类(partial)
在 C# 中,分部类(partial class)允许将一个类的定义分散到多个文件中。
通过使用 partial 关键字,你可以在多个文件中定义一个类、结构或接口。
这种功能通常用于组织大型代码文件,特别是在自动生成代码的情况下,手动编写的代码和自动生成的代码可以放在不同的文件中。
特点:
- 多个文件定义同一个类:一个类可以在多个文件中定义,只要每个部分都包含相同的访问修饰符,并且使用 partial 关键字。
- 编译时组合:尽管类定义分散在多个文件中,但在编译时,所有的部分会被组合成一个类。
- 一致的命名空间:所有分部类的定义必须位于相同的命名空间中。
- 支持字段、方法等的分离:可以将类的字段、属性、方法、事件等成员分散到不同的文件中进行定义。
- 常见于自动生成代码的场景:例如,使用 Visual Studio 自动生成代码时,设计器工具可能会创建一个 partial 类,开发者可以在另一个文件中编写额外的逻辑,而无需担心被自动生成的代码覆盖。
示例:
// File1.cs:
public partial class Person
{
// 字段和属性部分
public string Name { get; set; }
public int Age { get; set; }
}
// File2.cs:
public partial class Person
{
// 方法部分
public void DisplayInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
使用场景
- 代码组织:当一个类变得非常庞大时,可以使用分部类将代码逻辑分解到不同的文件中,方便管理和维护。
- 自动生成代码:在一些开发工具中(如 Visual Studio 的设计器工具),自动生成的代码通常会与手动编写的代码分开存放。
- 大型团队协作:在多人协作的项目中,开发者可以在不同的文件中对同一个类进行修改,而不会产生冲突。
规则和限制
- 必须在同一个命名空间中:所有分部类的定义必须在相同的命名空间中。
- 必须使用相同的访问修饰符:所有分部类的部分必须具有相同的访问级别(如 public、internal 等)。
- 静态和实例成员:分部类的不同部分可以包含静态和实例成员,也可以包含构造函数和析构函数。
- 类的完整性:即使类的定义被拆分到多个文件中,在编译时,编译器仍然会将它们视为同一个完整的类。因此,所有的成员都可以在整个类中进行访问,就像它们定义在同一个文件中一样。
分部方法 C# 中还支持 分部方法(partial method),允许你在一个文件中声明一个方法,而在另一个文件中定义它的实现。
// File1.cs:
public partial class Person
{
// 声明分部方法
partial void OnNameChanged();
public void ChangeName(string newName)
{
Name = newName;
OnNameChanged(); // 调用分部方法
}
}
// File2.cs:
public partial class Person
{
// 实现分部方法
partial void OnNameChanged()
{
Console.WriteLine($"Name has been changed to: {Name}");
}
}
接口
在 C# 中,接口(interface)是一个定义了行为契约的类型,
它规定了类或结构必须实现的方法、属性、事件和索引器,但不提供具体的实现。
接口是实现多重继承、解耦和定义公共行为的核心工具。
特点
- 只定义行为,不提供实现:接口只定义了成员(如方法、属性、事件、索引器)的签名,不提供任何实际的实现。
- 可以被多个类实现:一个类可以实现多个接口,弥补了 C# 不支持多继承的缺陷。
- 多态性:接口可以用来实现多态性。通过接口引用,调用实现了该接口的对象的行为,而不关心其具体类型。
- 不能包含字段:接口不能包含实例字段,所有的成员必须是方法、属性、事件或索引器。
- 默认是 public:接口成员默认是 public,不能使用访问修饰符进行修饰。
示例:
// 定义一个接口
public interface IAnimal
{
// 接口成员
void MakeSound(); // 方法
string Name { get; set; } // 属性
}
// 实现接口的类
public class Dog : IAnimal
{
// 实现接口成员
public string Name { get; set; }
public void MakeSound()
{
Console.WriteLine("Dog barks.");
}
}
public class Cat : IAnimal
{
public string Name { get; set; }
public void MakeSound()
{
Console.WriteLine("Cat meows.");
}
}
class Program
{
static void Main()
{
IAnimal dog = new Dog { Name = "Buddy" };
IAnimal cat = new Cat { Name = "Whiskers" };
dog.MakeSound(); // 输出: Dog barks.
cat.MakeSound(); // 输出: Cat meows.
}
}
多重接口实现
一个类可以实现多个接口,这为实现多重继承提供了灵活性。每个接口中的成员都必须实现。
示例:
public interface IAnimal
{
void MakeSound();
}
public interface IPet
{
string Owner { get; set; }
void Play();
}
public class Dog : IAnimal, IPet
{
public string Owner { get; set; }
public void MakeSound()
{
Console.WriteLine("Dog barks.");
}
public void Play()
{
Console.WriteLine("Dog plays fetch.");
}
}
class Program
{
static void Main()
{
Dog dog = new Dog { Owner = "John" };
dog.MakeSound(); // 输出: Dog barks.
dog.Play(); // 输出: Dog plays fetch.
}
}
接口的新特性(C# 8.0+)
从 C# 8.0 开始,接口可以包含 默认实现。
这意味着接口不仅可以定义行为,还可以为某些方法提供默认的实现。
这给了接口更多的灵活性,但依然保持了与传统接口的兼容性。
示例(带有默认实现的接口):
public interface IAnimal
{
void MakeSound();
// 默认实现
void Sleep()
{
Console.WriteLine("The animal is sleeping.");
}
}
public class Dog : IAnimal
{
public void MakeSound()
{
Console.WriteLine("Dog barks.");
}
}
class Program
{
static void Main()
{
IAnimal dog = new Dog();
dog.MakeSound(); // 输出: Dog barks.
dog.Sleep(); // 输出: The animal is sleeping.
}
}