泛型与预处理指令学习总结


一、泛型协变与逆变


1.1 协变(Covariant)

1.1.1 核心概念

协变是在.NET 4.0引入的特性,使用out关键字修饰泛型类型参数,控制返回值的类型

转换方向:子类泛型 → 父类泛型(正向兼容,类似隐式类型转换)

适用范围:仅泛型接口和泛型委托

1.2.2 语法格式
// 泛型接口协变
public interface IEnumerable<out T>

// 泛型委托协变
public delegate TResult Func<out TResult>();
1.2.3 代码示例
// 定义基类和子类
public class Bird { }
public class Sparrow : Bird { }

// 普通泛型无协变标记,无法隐式转换
List<Sparrow> sparrows = new List<Sparrow>();
// List<Bird> birds = sparrows; // 编译报错!

// 使用协变接口
IEnumerable<Bird> birdList1 = new List<Bird>();      // Bird → Bird
IEnumerable<Bird> birdList2 = new List<Sparrow>();   // Sparrow → Bird(协变)

// 内置协变类型示例
Func<int, Sparrow> f1 = a => new Sparrow();
Func<int, Bird> f2 = f1; // 协变:子类返回值 → 父类返回值
1.2.4 内置协变类型
类型 说明
IEnumerable<out T> 可枚举接口
IReadOnlyList<out T> 只读列表接口
Func<out TResult> 有返回值委托

1.3 逆变(Contravariant)

1.3.1 核心概念

逆变使用in关键字修饰泛型类型参数,控制传入参数的类型

转换方向:父类泛型 → 子类泛型(反向兼容,类似强制类型转换)

适用范围:仅泛型接口和泛型委托

1.3.2 语法格式
// 泛型接口逆变
public interface IComparer<in T>

// 泛型委托逆变
public delegate bool Predicate<in T>(T obj);
public delegate void Action<in T>(T obj);
1.3.3 代码示例
// 逆变:父类泛型 → 子类泛型
Predicate<Bird> birdPredicate = a => true;
Predicate<Sparrow> sparrowPredicate = birdPredicate; // 逆变

Action<object> objectAction = a => { };
Action<Bird> birdAction = objectAction; // 逆变

Func<Bird, int> birdFunc = a => 10;
Func<Sparrow, int> sparrowFunc = birdFunc; // 逆变(参数逆变)
1.3.4 内置逆变类型
类型 说明
Action<in T> 无返回值委托
IComparer<in T> 比较器接口
IEqualityComparer<in T> 相等比较器接口
Predicate<in T> 返回bool的委托

1.4 协变与逆变同时发生

Func<in T, out TResult>是同时支持协变和逆变的典型例子:

// Func<in T, out TResult>:参数T逆变,返回值TResult协变

// 逆变演示:参数类型从Bird(父类) → Sparrow(子类)
Func<Bird, decimal> f1 = a => 1M;
Func<Sparrow, decimal> f2 = f1; // 逆变

// 协变演示:返回值类型从Sparrow(子类) → Bird(父类)
Func<int, Sparrow> f3 = x => new Sparrow();
Func<int, Bird> f4 = f3; // 协变

1.5 判断协变与逆变的关键要点

协变判断要点
  1. 变量名前必须是泛型接口或泛型委托
  2. 泛型参数T前有**out关键字**
  3. 转换方向:子类泛型 → 父类泛型(正向兼容)
逆变判断要点
  1. 变量名前必须是泛型接口或泛型委托
  2. 泛型参数T前有**in关键字**
  3. 转换方向:父类泛型 → 子类泛型(反向兼容)
速记口诀
out 协变:输出、只读,子转父(Sparrow → Bird)
in 逆变:输入、只写,父转子(Bird → Sparrow)
类/结构体不能标记 in/out,仅接口、委托支持
out 类型只能当返回值;in 类型只能当参数

二、预处理指令

2.1 预处理指令概述

2.1.1 定义

预处理指令指示编译器如何处理源代码,用于控制代码的编译行为。例如在某些情况下忽略部分代码,在其他情况下编译该代码。

注意:C#没有独立的预处理器,预处理指令实际上由编译器处理。

2.1.2 基本规则
规则 说明
独立行 预处理指令必须和 C# 代码在不同的行
无分号 不需要以分号 ; 结尾
# 开头 每行必须以 # 字符开始(# 前可有空格,# 与指令间也可有空格)
允许行尾注释 #define Debug // 这是注释
禁止行内块注释 #define /* 注释 */ Debug
位置限制 #define / #undef 必须在文件最顶部,using 语句之前
2.1.3 指令列表
指令 功能
#define 符号 定义编译符号
#undef 符号 取消定义编译符号
#if 条件 条件编译开始
#elif 条件 条件编译分支
#else 条件编译默认分支
#endif 条件编译结束
#error 消息 触发编译错误,中断编译
#warning 消息 触发编译警告
#line 行号 "文件名" 修改编译器报告的行号/文件名
#line default 恢复编译器自动行号
#region 名称 定义可折叠代码块(编辑器功能)
#endregion 结束 #region

2.2 常用预处理指令详解

2.2.1 #define#undef
#define Debug          // 定义编译符号Debug
#define Log            // 定义编译符号Log
#undef Log            // 取消定义Log

// 注意:#define必须放在文件最顶部,using之前
2.2.2 #if#elif#else#endif
// 来源:C# 预处理器指令.md — Debug/Trace 环境区分
#if DEBUG
    Console.WriteLine("调试模式代码");
#elif TRACE
    Console.WriteLine("发布模式代码");
#elif Release
    Console.WriteLine("发布模式代码");
#else
    Console.WriteLine("其他配置");
#endif
2.2.3 Debug与Release环境差异
环境 默认定义的编译符号 用途
Debug DEBUGTRACE 调试环境,开发阶段使用
Release TRACE(无DEBUG) 发布环境,用户使用

Debug类(命名空间:System.Diagnostics.Debug):

  • 仅当定义DEBUG编译符号时生效
  • Release模式下,所有Debug.Write/Assert代码会被编译器直接删除
  • 无任何性能开销

Trace类(命名空间:System.Diagnostics.Trace):

  • 只要定义TRACE符号就生效
  • Debug、Release配置默认都开启TRACE
  • 线上发布包代码保留,可用于线上日志、监控
2.2.4 #error 编译错误指令
// 强制只能Debug编译
#if !DEBUG
#error 禁止使用Release模式编译当前模块,请切换Debug配置
#endif

// 调试工具类防护
#if !DEBUG
#error 此工具类仅限Debug调试,Release模式禁止编译打包
#endif
public static class DebugTool
{
    public static void ShowDebugData()
    {
#if !DEBUG
        throw new InvalidOperationException("禁止在发布环境调用调试专用方法");
#endif
        Debug.WriteLine("打印调试数据");
    }
}

#error作用:编译阶段直接抛出编译错误,中断编译流程,控制台/错误列表显示自定义提示文字。

2.2.5 #warning 警告指令
#warning 此处逻辑未完成,上线前必须补充校验
void Test()
{
    // 待完善代码
}

#warning作用:生成编译警告,提醒开发者注意某些未完成或有问题的代码。

2.2.6 #line 行号指令
// 让下一行代码视为第46行,文件名为Form1.cs
#line 46 "Form1.cs"
int a = 10;
Console.WriteLine(a / 0); // 报错会提示Form1.cs第46行

// 恢复编译器自动统计行号
#line default

#line作用:修改编译器报告错误和警告时使用的行号和文件名,常用于代码生成工具。

2.2.7 #region#endregion
#region 数据访问层
// 数据库连接代码
// SQL操作代码
#endregion

#region 业务逻辑层
// 业务处理代码
#endregion

作用:在Visual Studio编辑器中创建可折叠的代码区域,提高代码可读性。


2.3 实用技巧

2.3.1 条件编译实现多环境配置
#if DEBUG
    string connectionString = "Server=localhost;Database=TestDB;";
#elif TEST
    string connectionString = "Server=test-server;Database=TestDB;";
#else
    string connectionString = "Server=prod-server;Database=ProdDB;";
#endif
2.3.2 调试代码隔离
public void ProcessData(List<string> data)
{
#if DEBUG
    Debug.WriteLine($"数据条数:{data.Count}");
    foreach (var item in data)
    {
        Debug.WriteLine($"数据项:{item}");
    }
#endif
    
    // 实际业务逻辑
    foreach (var item in data)
    {
        // 处理逻辑
    }
}
2.3.3 API版本控制
#define API_V2

public class ApiClient
{
#if API_V2
    public async Task<string> GetDataV2(string url)
    {
        // V2版本实现
        return await httpClient.GetStringAsync(url + "/v2");
    }
#else
    public async Task<string> GetData(string url)
    {
        // V1版本实现
        return await httpClient.GetStringAsync(url);
    }
#endif
}
2.3.4 防止调试代码泄露
#if !DEBUG
#error 此模块包含调试代码,禁止在Release模式下编译
#endif

public class DebugOnlyModule
{
    // 调试专用代码
}

三、总结

泛型协变与逆变核心要点

  1. 协变(out):控制返回值,子类泛型可赋值给父类泛型
  2. 逆变(in):控制输入参数,父类泛型可赋值给子类泛型
  3. 适用范围:仅泛型接口和泛型委托,类和结构体不支持
  4. 判断依据:变量类型必须是接口/委托,且泛型参数标记了out/in

预处理指令核心要点

  1. 编译时控制:预处理指令在编译阶段生效,影响代码的编译结果
  2. 环境适配:通过条件编译实现Debug/Release等不同环境的代码适配
  3. 代码保护:使用#error防止调试代码泄露到生产环境
  4. 代码组织:使用#region提高代码可读性

更多推荐