以下是对提供的 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<Students>
集合中的元素,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类型索引器
四、索引器与数组的区别
特性 | 索引器 | 数组 |
---|---|---|
本质 | 类的成员(方法的语法糖) | 引用类型(数据结构) |
索引类型 | 任意类型(int、string 等) | 只能是 int 类型 |
长度灵活性 | 可动态调整(内部逻辑控制) | 长度固定(创建后不可变) |
定义位置 | 类或结构内部 | 独立定义(变量) |
五、代码细节分析与扩展
1. 第一个代码(ClassRoom 类)
内部数据结构:使用
List<Students>
存储学生,索引器封装了对 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<T>
、Dictionary<TKey, TValue>
内部都实现了索引器)。
二、接口(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. 与抽象类的区别
对比项 | 接口 | 抽象类 |
---|---|---|
实现方式 | 类通过: 实现,可多实现 | 类通过: 继承,仅单继承 |
成员实现 | 无实现(纯规范) | 可包含抽象成员(无实现)和具体成员 |
访问修饰符 | 成员无修饰符(默认公开) | 成员可加修饰符(public、protected 等) |
成员类型 | 仅属性、方法、事件、索引器 | 可包含字段、属性、方法等 |
实例化 | 不能实例化 | 不能实例化 |
一、接口的本质与核心特性
接口是 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的C IB 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() { /* 实现 */ } }
五、接口与抽象类的对比(补充完整)
特性 | 接口 | 抽象类 |
---|---|---|
实例化 | 不能实例化 | 不能实例化 |
成员实现 | 所有成员无实现(纯规范) | 可以包含抽象成员(无实现)和非抽象成员(有实现) |
继承 / 实现方式 | 类 / 结构体通过: 实现,支持多实现 | 类通过: 继承,仅支持单继承 |
成员访问修饰符 | 默认 public,不能显式添加修饰符 | 可以有 public、protected 等修饰符 |
包含的成员类型 | 只能有属性、方法、索引器、事件(无字段) | 可以有字段、属性、方法、索引器、事件等 |
关系本质 | 表示 “具有某种能力”(has-a) | 表示 “是一种”(is-a) |
结构体支持 | 结构体可以实现接口 | 结构体不能继承抽象类(结构体是值类型) |
六、接口的典型应用场景
定义规范:为不同类提供统一的行为标准(如
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>
,参数或返回值使用T
作为类型。 示例:public static T Fn<T>(T i) { return i; }
泛型接口:接口名后加
<T>
,成员使用T
作为类型。 示例:interface ICalc<T> { T Add(T a, T b); }
泛型类:类名后加
<T>
,成员使用T
作为类型。 示例:class Calc3<T> : ICalc<T> { ... }
2. 核心特点
类型推断:调用泛型方法时,可省略类型指定(编译器根据参数自动推断)。 例如:
Fn(123)
等价于Fn<int>(123)
。多泛型参数:支持多个泛型参数(如
<T1, T2>
),分别指定不同类型。 示例:public static T1 Fn3<T1, T2>(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 参数) {// 逻辑实现,T可作为参数类型、返回值类型或局部变量类型 } // 调用方式 方法名<int>(123); // 显式指定类型 方法名("hello"); // 隐式推断类型(T=string)
示例解析(用户代码):
public static T Fn<T>(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 方法名(T 参数); } // 实现方式1:指定具体类型 class 类名 : I接口名<int> {public int 方法名(int 参数) { /* 实现 */ } } // 实现方式2:继续使用泛型(泛型类实现泛型接口) class 类名<T> : I接口名<T> {public T 方法名(T 参数) { /* 实现 */ } }
示例解析(用户代码):
// 泛型接口ICalc<T> interface ICalc<T> {T Add(T a, T b);T Sub(T a, T b); } // 实现1:指定T=int class Calc : ICalc<int> { /* 实现int类型的加减 */ } // 实现2:指定T=string class Calc2 : ICalc<string> { /* 实现string类型的加减 */ }
3. 泛型类
类定义时包含类型参数,实例化时需指定具体类型。
语法与特性:
// 定义泛型类 class 类名<T> {private T 字段;public T 方法(T 参数) { /* 实现 */ } } // 实例化 var 变量 = new 类名<int>(); // T被替换为int
示例解析(用户代码):
class Calc3<T> : ICalc<T> {public T Add(T a, T b){return default(T); // default(T)返回T类型的默认值} } // 使用时指定类型 var calc = new Calc3<double>(); double result = calc.Add(1.5, 2.5); // result=0.0(double默认值)
三、泛型的性能优势(基于测试代码)
用户提供的_08_泛型测试
代码通过计时器对比了三种方式的性能:
方法类型 | 实现方式 | 10000 次调用耗时(示例值) | 性能差异原因 |
---|---|---|---|
ShowInt | 具体类型(int) | 187ms | 无类型转换,直接操作 |
ShowObject | object 类型(装箱 / 拆箱) | 235ms | int→object(装箱)和 object→int(拆箱)消耗性能 |
Show<T> | 泛型方法 | 220ms | 编译时生成具体类型代码,无装箱 / 拆箱 |
结论:泛型性能接近具体类型方法,远优于 object 类型(避免了值类型与引用类型转换的开销)。
四、泛型的关键特性
类型推断 调用泛型方法时,若编译器可从参数推断出类型,可省略
<类型>
:Fn2(1, new int[] { 1 }); // 推断T=int Fn3(1, new string[] { "a" }); // 推断TTest1=int,TTest2=string
多类型参数 泛型可包含多个类型参数(用
,
分隔):public static T1 Fn3<T1, T2>(T1 i, T2[] arr) { return i; }
默认值(default (T)) 用于获取任意类型的默认值(值类型为
0
/false
等,引用类型为null
):return default(T); // 泛型中安全获取默认值
五、泛型与其他技术的对比
技术 | 优势 | 劣势 | 适用场景 |
---|---|---|---|
泛型 | 类型安全、性能好、代码复用 | 语法稍复杂 | 通用逻辑(如集合、工具类) |
重载方法 | 简单直观 | 类型增多时代码量爆炸 | 类型较少的场景 |
object 类型 | 灵活(支持所有类型) | 性能差(装箱)、类型不安全(需强制转换) | 早期版本 C#(无泛型时) |
总结
泛型是 C# 中实现 “编写一次,适配多类型” 的核心机制,通过泛型方法、泛型类、泛型接口三种形式,在保证类型安全和性能的前提下,极大提升了代码复用率。其设计思想贯穿于.NET Framework 的核心组件(如List<T>
、Dictionary<TKey, TValue>
),是 C# 开发者必须掌握的重要特性。