c#软件开发学习笔记--逆变与协变、预处理指令
·
泛型与预处理指令学习总结
一、泛型协变与逆变
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 判断协变与逆变的关键要点
协变判断要点
- 变量名前必须是泛型接口或泛型委托
- 泛型参数T前有**
out关键字** - 转换方向:子类泛型 → 父类泛型(正向兼容)
逆变判断要点
- 变量名前必须是泛型接口或泛型委托
- 泛型参数T前有**
in关键字** - 转换方向:父类泛型 → 子类泛型(反向兼容)
速记口诀
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 | DEBUG、TRACE |
调试环境,开发阶段使用 |
| 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
{
// 调试专用代码
}
三、总结
泛型协变与逆变核心要点
- 协变(out):控制返回值,子类泛型可赋值给父类泛型
- 逆变(in):控制输入参数,父类泛型可赋值给子类泛型
- 适用范围:仅泛型接口和泛型委托,类和结构体不支持
- 判断依据:变量类型必须是接口/委托,且泛型参数标记了out/in
预处理指令核心要点
- 编译时控制:预处理指令在编译阶段生效,影响代码的编译结果
- 环境适配:通过条件编译实现Debug/Release等不同环境的代码适配
- 代码保护:使用#error防止调试代码泄露到生产环境
- 代码组织:使用#region提高代码可读性
更多推荐
所有评论(0)