> 技术文档 > C#索引器、接口、泛型

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类型索引器
四、索引器与数组的区别
特性 索引器 数组 本质 类的成员(方法的语法糖) 引用类型(数据结构) 索引类型 任意类型(int、string 等) 只能是 int 类型 长度灵活性 可动态调整(内部逻辑控制) 长度固定(创建后不可变) 定义位置 类或结构内部 独立定义(变量)
五、代码细节分析与扩展
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; // 索引正常时直接赋值   }}

    这个逻辑解决了数组长度固定的问题,通过索引器对外提供 “动态数组” 的体验。

六、索引器的扩展用法
  1. 多参数索引器:支持多个参数(类似二维数组),例如:

    // 二维索引器:访问矩阵中的元素public int this[int row, int col]{    get { return matrix[row, col]; }    set { matrix[row, col] = value; }}// 调用:matrix[2, 3] = 10;
  2. 限制访问权限:通过访问修饰符控制 get/set 的可见性,例如:

    public string this[int index]{    get { return data[index]; } // 公开可读    private set { data[index] = value; } // 仅类内部可写}
  3. 结合接口:索引器可以在接口中定义(仅声明,无实现),由实现类具体实现:

    public interface IIndexable{    string this[int index] { get; set; }}
总结

索引器是 C# 中增强类交互性的重要特性,通过模拟数组的访问方式,简化了对类内部数据的操作。其核心优势在于:灵活的索引类型、支持重载、可封装复杂内部逻辑,常用于集合类、数据容器等场景(如ListDictionary内部都实现了索引器)。

二、接口(Interface)

接口是一种规范(“契约”),定义了一组必须实现的成员(属性、方法等),但不包含实现逻辑,由类或结构体实现。

1. 基本定义与格式
  • 作用:统一不同类的行为标准,实现 “多态” 和 “解耦”。

  • 格式:

    interface 接口名(通常以I开头){    // 成员声明(无访问修饰符,默认公开)    返回值类型 方法名(参数);    类型 属性名 { get; set; }}
  • 实现规则:类 / 结构体通过:实现接口,必须实现接口中所有成员(包括继承的父接口成员)。

2. 核心特点
  • 多实现:一个类可以实现多个接口(用,分隔),解决类的单继承限制。 例如:Book类同时实现IBookIPaper接口。

  • 接口继承:接口可以继承其他接口,子接口包含父接口的所有成员。实现子接口的类必须实现所有父接口和子接口的成员。 例如:IStudent继承IPeopleStudent类实现IStudent时,需实现IPeopleNameAgeIStudentStudentIdStudy

  • 显式实现:当多个接口包含同名不同类型的成员时,需显式实现(不添加访问修饰符,通过 “接口名。成员” 定义)。 例如:IAIBC属性(int 和 string 类型),通过int IA.Cstring 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) 结构体支持 结构体可以实现接口 结构体不能继承抽象类(结构体是值类型)
六、接口的典型应用场景
  1. 定义规范:为不同类提供统一的行为标准(如ICollection接口规定集合的基本操作)。

  2. 多态实现:通过接口类型变量调用不同实现类的方法,实现 “同一接口,不同行为”。

    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();  // 输出\"飞机飞\"
  3. 解耦设计:降低类之间的依赖(如依赖注入中,通过接口注入而非具体类)。

总结

接口是 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 : classT必须是引用类型。

  • where T : structT必须是值类型。

  • 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_泛型测试代码通过计时器对比了三种方式的性能:

方法类型 实现方式 10000 次调用耗时(示例值) 性能差异原因 ShowInt 具体类型(int) 187ms 无类型转换,直接操作 ShowObject object 类型(装箱 / 拆箱) 235ms int→object(装箱)和 object→int(拆箱)消耗性能 Show 泛型方法 220ms 编译时生成具体类型代码,无装箱 / 拆箱

结论:泛型性能接近具体类型方法,远优于 object 类型(避免了值类型与引用类型转换的开销)。

四、泛型的关键特性
  1. 类型推断 调用泛型方法时,若编译器可从参数推断出类型,可省略

    Fn2(1, new int[] { 1 });  // 推断T=intFn3(1, new string[] { \"a\" });  // 推断TTest1=int,TTest2=string
  2. 多类型参数 泛型可包含多个类型参数(用,分隔):

    public static T1 Fn3(T1 i, T2[] arr) { return i; }
  3. 默认值(default (T)) 用于获取任意类型的默认值(值类型为0/false等,引用类型为null):

    return default(T);  // 泛型中安全获取默认值
五、泛型与其他技术的对比
技术 优势 劣势 适用场景 泛型 类型安全、性能好、代码复用 语法稍复杂 通用逻辑(如集合、工具类) 重载方法 简单直观 类型增多时代码量爆炸 类型较少的场景 object 类型 灵活(支持所有类型) 性能差(装箱)、类型不安全(需强制转换) 早期版本 C#(无泛型时)
总结

泛型是 C# 中实现 “编写一次,适配多类型” 的核心机制,通过泛型方法、泛型类、泛型接口三种形式,在保证类型安全和性能的前提下,极大提升了代码复用率。其设计思想贯穿于.NET Framework 的核心组件(如ListDictionary),是 C# 开发者必须掌握的重要特性。