C#索引器、接口、泛型
以下是对提供的 C# 代码中涉及的核心知识点的梳理和总结,涵盖索引器、接口、泛型三大核心内容,以及相关实践要点:
一、索引器(Indexer)
索引器是一种允许类或结构体像数组一样通过[]
语法访问成员的特殊成员,本质是对类中数据的 “索引式访问” 封装。
1. 基本定义与格式
-
作用:让对象可以通过
对象名[索引]
的方式访问内部数据(如数组、集合中的元素),简化访问逻辑。 -
格式:
public 返回值类型 this[索引类型 索引参数名]{ get { /* 获取数据时执行,返回对应值 */ } set { /* 设置数据时执行,value为赋值内容 */ }}
-
关键说明:
-
this
关键字表示当前对象,索引参数可以是任意类型(int、string 等)。 -
get
块:通过索引获取数据时触发,返回内部存储的数据。 -
set
块:通过索引设置数据时触发,value
是赋值运算符右侧的值(若省略set
,则索引器为只读)。
-
2. 核心特点
-
支持多类型索引:同一个类可以定义多个索引器(重载),通过索引参数类型区分。 例如:
ClassRoom
类同时定义this[int index]
(按位置索引)和this[string name]
(按姓名索引)。 -
动态处理逻辑:可在
get/set
中添加自定义逻辑(如边界检查、数据转换)。 例如:索引器练习中,当索引超出数组长度时,动态扩展数组长度。 -
与数组的区别:数组的索引固定为
int
类型且基于连续内存,索引器的索引类型和内部实现可自定义(如基于字典、集合)。
3. 示例解析
-
在
ClassRoom
类中,通过this[int index]
索引器访问List
集合中的元素,get
返回对应索引的学生对象,set
修改对应位置的学生对象。 -
通过
this[string name]
索引器,根据姓名查找学生(使用List.Find
方法),实现按姓名索引的功能。
一、索引器的本质与作用
索引器是 C# 中一种特殊的类成员,允许类或结构的实例像数组一样通过索引([]
) 进行访问,从而简化对内部数据的操作。其核心作用是:将类的内部数据结构(如数组、集合)封装起来,对外提供类似数组的访问接口,同时隐藏内部实现细节。
二、索引器的基本语法
// 访问修饰符 返回值类型 this[参数类型 参数名]public 数据类型 this[索引类型 index]{ get { /* 获取值的逻辑,返回对应数据 */ } set { /* 设置值的逻辑,value表示赋值的内容 */ }}
-
this
:特殊关键字,代表当前类的实例(类似属性,但索引器没有名称,通过this
标识)。 -
索引类型
:可以是任意类型(int、string、自定义类型等),这是索引器与数组的关键区别(数组索引只能是 int)。 -
get访问器
:通过索引获取值时执行(类似数组的读操作)。 -
set访问器
:通过索引设置值时执行(类似数组的写操作),value
是隐式参数,代表赋值的内容。 -
若省略
set
,则索引器为只读;若省略get
,则为只写(通常不推荐)。
三、索引器的重载特性
索引器支持重载(与方法重载规则一致),即同一类中可以定义多个索引器,通过参数类型或参数数量区分。
示例(第一个代码): ClassRoom
类定义了两个索引器:
// 1. int类型索引:通过下标访问学生public Students this[int index] { get; set; }// 2. string类型索引:通过姓名查找学生public Students this[string n] { get; }
调用时会根据[]
中参数的类型自动匹配对应的索引器:
room[1]; // 匹配int类型索引器room[\"郑爽\"]; // 匹配string类型索引器
四、索引器与数组的区别
五、代码细节分析与扩展
1. 第一个代码(ClassRoom 类)
-
内部数据结构:使用
List
存储学生,索引器封装了对 List 的访问,避免直接暴露 List(封装性)。 -
string 类型索引器
:通过姓名查找学生,使用
List.Find()
方法结合 Lambda 表达式简化逻辑:
return students.Find(s => s.Name == n); // 等价于循环遍历查找,更简洁
-
set 访问器的作用:
this[int index]
的 set 访问器允许直接通过索引修改 List 中的元素,例如:
room[0] = new Students() { Name = \"金秀贤\", Sex=\'女\' }; // 实际执行students[0] = value;
2. 第二个代码(Student 类)
-
核心功能:索引器处理数组索引越界问题,实现动态扩展数组长度。
-
关键逻辑(set 访问器):
set { if (index >= names.Length) { // 索引越界时,创建新数组并复制原有元素 string[] newNames = new string[names.Length + 1]; Array.Copy(names, newNames, names.Length); // 复制旧数据 newNames[index] = value; // 赋值新元素 names = newNames; // 替换旧数组 } else { names[index] = value; // 索引正常时直接赋值 }}
这个逻辑解决了数组长度固定的问题,通过索引器对外提供 “动态数组” 的体验。
六、索引器的扩展用法
-
多参数索引器:支持多个参数(类似二维数组),例如:
// 二维索引器:访问矩阵中的元素public int this[int row, int col]{ get { return matrix[row, col]; } set { matrix[row, col] = value; }}// 调用:matrix[2, 3] = 10;
-
限制访问权限:通过访问修饰符控制 get/set 的可见性,例如:
public string this[int index]{ get { return data[index]; } // 公开可读 private set { data[index] = value; } // 仅类内部可写}
-
结合接口:索引器可以在接口中定义(仅声明,无实现),由实现类具体实现:
public interface IIndexable{ string this[int index] { get; set; }}
总结
索引器是 C# 中增强类交互性的重要特性,通过模拟数组的访问方式,简化了对类内部数据的操作。其核心优势在于:灵活的索引类型、支持重载、可封装复杂内部逻辑,常用于集合类、数据容器等场景(如List
、Dictionary
内部都实现了索引器)。
二、接口(Interface)
接口是一种规范(“契约”),定义了一组必须实现的成员(属性、方法等),但不包含实现逻辑,由类或结构体实现。
1. 基本定义与格式
-
作用:统一不同类的行为标准,实现 “多态” 和 “解耦”。
-
格式:
interface 接口名(通常以I开头){ // 成员声明(无访问修饰符,默认公开) 返回值类型 方法名(参数); 类型 属性名 { get; set; }}
-
实现规则:类 / 结构体通过
:
实现接口,必须实现接口中所有成员(包括继承的父接口成员)。
2. 核心特点
-
多实现:一个类可以实现多个接口(用
,
分隔),解决类的单继承限制。 例如:Book
类同时实现IBook
和IPaper
接口。 -
接口继承:接口可以继承其他接口,子接口包含父接口的所有成员。实现子接口的类必须实现所有父接口和子接口的成员。 例如:
IStudent
继承IPeople
,Student
类实现IStudent
时,需实现IPeople
的Name
、Age
和IStudent
的StudentId
、Study
。 -
显式实现:当多个接口包含同名不同类型的成员时,需显式实现(不添加访问修饰符,通过 “接口名。成员” 定义)。 例如:
IA
和IB
的C
属性(int 和 string 类型),通过int IA.C
和string IB.C
实现,访问时需将对象转为对应接口类型。
3. 与抽象类的区别
:
实现,可多实现:
继承,仅单继承一、接口的本质与核心特性
接口是 C# 中一种引用类型,它定义了一组未实现的成员规范(属性、方法、索引器、事件等),本质是一种 “契约” 或 “规则”。其核心特性包括:
-
无实现:接口只声明成员 “是什么”,不定义 “怎么做”(方法无方法体,属性只有
get/set
声明)。 -
强制实现:类或结构体实现接口时,必须全部实现接口中的所有成员,否则会编译错误。
-
多实现支持:一个类 / 结构体可以同时实现多个接口(弥补 C# 类单继承的限制)。
二、接口的定义语法
// 接口名称通常以\"I\"开头(约定),成员默认是public(不能显式添加访问修饰符)interface 接口名{ // 属性声明(无实现) 返回值类型 属性名 { get; set; } // 方法声明(无方法体) 返回值类型 方法名(参数列表); // 索引器、事件等(语法类似类成员,但无实现)}
示例(用户代码):
interface IBook{ string Name { get; set; } // 属性声明 double Price { get; set; } void Fn(); // 方法声明 void Fn(string n); // 方法重载声明}
三、接口的实现
类或结构体通过:
符号实现接口,需严格遵循接口规范:
1. 基本实现规则
-
必须实现接口中所有成员(包括重载的方法、属性等)。
-
实现的成员必须与接口声明的返回值、参数列表、名称完全一致。
-
类可以在实现接口的基础上,添加自己的额外成员(如
Book
类的Color
属性)。
示例:
class Book : IBook, IPaper // 实现多个接口{ // 实现IBook的属性 public string Name { get; set; } public double Price { get; set; } // 实现IPaper的属性 public string Type { get; set; } // 类自己的额外成员 public string Color { get; set; } // 实现IBook的方法 public void Fn() { /* 具体实现 */ } public void Fn(string n) { /* 具体实现 */ }}
2. 显式实现(解决成员冲突)
当类实现的多个接口包含同名成员(且类型 / 参数不同)时,需使用显式实现避免冲突:
-
语法:
接口名.成员名
(无访问修饰符)。 -
显式实现的成员只能通过接口类型的变量访问,不能通过类实例直接访问。
示例(用户代码):
interface IA { int C { get; set; } }interface IB { string C { get; set; } }class Test : IA, IB{ // 显式实现IA的C(int类型) int IA.C { get; set; } // 显式实现IB的C(string类型) string IB.C { get; set; }}// 调用方式Test test = new Test();IA ia = test;ia.C = 10; // 访问IA的CIB ib = test;ib.C = \"hello\"; // 访问IB的C
四、接口的继承
接口支持多继承(与类不同,类只能单继承),即一个接口可以继承多个其他接口,继承后会包含父接口的所有成员。
规则:
-
接口继承语法:
interface 子接口 : 父接口1, 父接口2...
。 -
类实现子接口时,必须同时实现子接口和所有父接口的成员。
示例(用户代码):
// IStudent继承IPeople,包含IPeople的所有成员interface IStudent : IPeople{ string StudentId { get; set; } void Study();}// 实现IStudent必须同时实现IPeople的成员class Student : IStudent{ // 实现IPeople的成员 public string Name { get; set; } public int Age { get; set; } // 实现IStudent的成员 public string StudentId { get; set; } public void Study() { /* 实现 */ }}
五、接口与抽象类的对比(补充完整)
:
实现,支持多实现:
继承,仅支持单继承六、接口的典型应用场景
-
定义规范:为不同类提供统一的行为标准(如
ICollection
接口规定集合的基本操作)。 -
多态实现:通过接口类型变量调用不同实现类的方法,实现 “同一接口,不同行为”。
interface IFly { void Fly(); }class Bird : IFly { public void Fly() { Console.WriteLine(\"鸟飞\"); } }class Plane : IFly { public void Fly() { Console.WriteLine(\"飞机飞\"); } }// 多态调用IFly fly1 = new Bird();IFly fly2 = new Plane();fly1.Fly(); // 输出\"鸟飞\"fly2.Fly(); // 输出\"飞机飞\"
-
解耦设计:降低类之间的依赖(如依赖注入中,通过接口注入而非具体类)。
总结
接口是 C# 中实现 “规范与实现分离” 的核心机制,通过强制实现、多实现支持、多继承能力,灵活解决了类单继承的局限,是实现多态、规范设计的重要工具。理解接口与抽象类的区别,能帮助在不同场景下选择更合适的设计方式(需要代码复用选抽象类,需要多能力规范选接口)。
三、泛型(Generic)
泛型是一种 “延迟指定类型” 的语法,允许在定义方法、类、接口时不指定具体类型,而在使用时动态指定,解决代码复用和类型安全问题。
1. 基本定义与格式
-
作用:避免为不同类型重复编写相同逻辑(如 int、string 的通用方法),同时避免装箱拆箱(提升性能)。
-
常见形式
:
-
泛型方法:方法名后加
,参数或返回值使用
T
作为类型。 示例:public static T Fn(T i) { return i; }
-
泛型接口:接口名后加
,成员使用
T
作为类型。 示例:interface ICalc { T Add(T a, T b); }
-
泛型类:类名后加
,成员使用
T
作为类型。 示例:class Calc3 : ICalc { ... }
-
2. 核心特点
-
类型推断:调用泛型方法时,可省略类型指定(编译器根据参数自动推断)。 例如:
Fn(123)
等价于Fn(123)
。 -
多泛型参数:支持多个泛型参数(如
),分别指定不同类型。 示例:
public static T1 Fn3(T1 i, T2[] arr) { ... }
-
默认值:通过
default(T)
获取泛型类型的默认值(如引用类型为null
,值类型为0
)。 -
性能优势:相比
object
参数(需装箱拆箱),泛型直接操作具体类型,减少性能损耗(如泛型测试中,泛型方法比object
参数方法更快)。
3. 泛型约束(补充)
泛型默认支持所有类型,但可通过约束限制T
的范围(如仅允许引用类型、特定接口的实现类等),语法:where T : 约束条件
。 常见约束:
-
where T : class
:T
必须是引用类型。 -
where T : struct
:T
必须是值类型。 -
where T : 接口名
:T
必须实现指定接口。 -
where T : 类名
:T
必须是指定类或其派生类。
一、泛型的本质与价值
泛型是 C# 中一种参数化类型的机制,允许在定义类、方法、接口时不指定具体类型,而是在使用时动态指定。其核心价值在于:
-
代码复用:一套逻辑适配多种数据类型(避免为 int、string、自定义类型重复编写相同代码)。
-
类型安全:编译时检查类型匹配(相比 object 类型转换,减少运行时错误)。
-
性能优化:避免值类型与引用类型之间的装箱 / 拆箱操作(见泛型测试代码分析)。
二、泛型的三种基本形式
1. 泛型方法
在方法名后添加,调用时指定具体类型(或由编译器自动推断)。
语法与特性:
// 定义泛型方法public static 返回值类型 方法名(T 参数){ // 逻辑实现,T可作为参数类型、返回值类型或局部变量类型}// 调用方式方法名(123); // 显式指定类型方法名(\"hello\"); // 隐式推断类型(T=string)
示例解析(用户代码):
public static T Fn(T i) { return i; }// 调用时T被替换为具体类型,等价于:// public static int Fn(int i) { return i; }// public static string Fn(string i) { return i; }
2. 泛型接口
接口定义时包含类型参数,实现接口时需指定具体类型或继续使用泛型。
语法与特性:
// 定义泛型接口interface I接口名{ T 方法名(T 参数);}// 实现方式1:指定具体类型class 类名 : I接口名{ public int 方法名(int 参数) { /* 实现 */ }}// 实现方式2:继续使用泛型(泛型类实现泛型接口)class 类名 : I接口名{ public T 方法名(T 参数) { /* 实现 */ }}
示例解析(用户代码):
// 泛型接口ICalcinterface ICalc{ T Add(T a, T b); T Sub(T a, T b);}// 实现1:指定T=intclass Calc : ICalc { /* 实现int类型的加减 */ }// 实现2:指定T=stringclass Calc2 : ICalc { /* 实现string类型的加减 */ }
3. 泛型类
类定义时包含类型参数,实例化时需指定具体类型。
语法与特性:
// 定义泛型类class 类名{ private T 字段; public T 方法(T 参数) { /* 实现 */ }}// 实例化var 变量 = new 类名(); // T被替换为int
示例解析(用户代码):
class Calc3 : ICalc{ public T Add(T a, T b) { return default(T); // default(T)返回T类型的默认值 }}// 使用时指定类型var calc = new Calc3();double result = calc.Add(1.5, 2.5); // result=0.0(double默认值)
三、泛型的性能优势(基于测试代码)
用户提供的_08_泛型测试
代码通过计时器对比了三种方式的性能:
ShowInt
ShowObject
Show
结论:泛型性能接近具体类型方法,远优于 object 类型(避免了值类型与引用类型转换的开销)。
四、泛型的关键特性
-
类型推断 调用泛型方法时,若编译器可从参数推断出类型,可省略
:
Fn2(1, new int[] { 1 }); // 推断T=intFn3(1, new string[] { \"a\" }); // 推断TTest1=int,TTest2=string
-
多类型参数 泛型可包含多个类型参数(用
,
分隔):public static T1 Fn3(T1 i, T2[] arr) { return i; }
-
默认值(default (T)) 用于获取任意类型的默认值(值类型为
0
/false
等,引用类型为null
):return default(T); // 泛型中安全获取默认值
五、泛型与其他技术的对比
总结
泛型是 C# 中实现 “编写一次,适配多类型” 的核心机制,通过泛型方法、泛型类、泛型接口三种形式,在保证类型安全和性能的前提下,极大提升了代码复用率。其设计思想贯穿于.NET Framework 的核心组件(如List
、Dictionary
),是 C# 开发者必须掌握的重要特性。