C# 中那些好用到离谱的黑科技:附代码示例和使用场景
前言
很多人写 C#,可能只停留在:
if else
for
foreach
class
DataTable
SqlConnection
这些基础用法上。
但实际上,C# 是一门非常强大的工程型语言。
它不仅适合写 WinForms、ASP.NET、WebAPI、桌面工具、后台服务,还内置了很多非常“高级”的能力。
比如:
反射
特性
扩展方法
泛型约束
表达式树
委托
动态类型
LINQ
异步编程
动态编译
调用堆栈
这些东西如果用得好,可以大幅提升开发效率,让代码更优雅、更通用、更容易维护。
下面就来聊聊 C# 中那些真正实用的“黑科技”。
一、扩展方法:不改源码,也能给类加方法
扩展方法是 C# 非常好用的语法糖。
它可以在不修改原类源码的情况下,给已有类型“增加方法”。
示例:判断字符串是否为空
public static class StringExtensions
{
public static bool IsNullOrEmptyEx(this string value)
{
return string.IsNullOrEmpty(value);
}
public static bool IsNullOrWhiteSpaceEx(this string value)
{
return string.IsNullOrWhiteSpace(value);
}
}
使用方式
string name = "";
if (name.IsNullOrEmptyEx())
{
Console.WriteLine("字符串为空");
}
好处
以前我们写:
string.IsNullOrEmpty(name)
使用扩展方法后,可以写成:
name.IsNullOrEmptyEx()
代码更接近自然语言。
适合场景
字符串处理
DataTable处理
List处理
时间格式化
金额格式化
控件操作封装
比如 WinForms 中可以封装控件扩展:
public static class ControlExtensions
{
public static void SafeSetText(this Control control, string text)
{
if (control.InvokeRequired)
{
control.Invoke(new Action(() =>
{
control.Text = text;
}));
}
else
{
control.Text = text;
}
}
}
使用:
label1.SafeSetText("处理完成");
这样就不用每次都写 InvokeRequired 了。
二、反射:运行时动态读取类、属性、方法
反射是 C# 中非常强大的能力。
它可以在程序运行时动态获取类型信息。
示例类
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
获取属性和值
User user = new User
{
Id = 1,
Name = "张三",
Age = 25
};
Type type = user.GetType();
foreach (PropertyInfo prop in type.GetProperties())
{
string propName = prop.Name;
object propValue = prop.GetValue(user, null);
Console.WriteLine(propName + " = " + propValue);
}
输出:
Id = 1
Name = 张三
Age = 25
好处
反射最大的价值是:
写通用代码
减少重复代码
提升框架化能力
使用场景
对象转字典
对象转SQL
对象转DataTable
ORM框架
插件系统
配置读取
自动映射
比如可以写一个对象转字典:
public static Dictionary<string, object> ToDictionary(object obj)
{
Dictionary<string, object> dict = new Dictionary<string, object>();
if (obj == null)
{
return dict;
}
Type type = obj.GetType();
foreach (PropertyInfo prop in type.GetProperties())
{
dict[prop.Name] = prop.GetValue(obj, null);
}
return dict;
}
使用:
User user = new User { Id = 1, Name = "李四", Age = 20 };
Dictionary<string, object> dict = ToDictionary(user);
三、特性 Attribute:给类和属性打“标签”
特性可以理解为给代码加元数据。
很多框架都大量使用特性,比如:
[Serializable]
[Obsolete]
[HttpGet]
[Required]
[Table]
[Column]
自定义特性
[AttributeUsage(AttributeTargets.Property)]
public class DisplayNameAttribute : Attribute
{
public string Name { get; private set; }
public DisplayNameAttribute(string name)
{
Name = name;
}
}
使用特性
public class User
{
[DisplayName("用户编号")]
public int Id { get; set; }
[DisplayName("用户姓名")]
public string Name { get; set; }
[DisplayName("年龄")]
public int Age { get; set; }
}
读取特性
Type type = typeof(User);
foreach (PropertyInfo prop in type.GetProperties())
{
object[] attrs = prop.GetCustomAttributes(typeof(DisplayNameAttribute), false);
if (attrs.Length > 0)
{
DisplayNameAttribute attr = attrs[0] as DisplayNameAttribute;
Console.WriteLine(prop.Name + " 显示名称:" + attr.Name);
}
}
好处
特性可以让代码更加声明式。
不用写一堆 if else,而是通过标签告诉程序应该怎么处理。
使用场景
字段中文名映射
Excel导出表头
数据库字段映射
接口权限控制
参数校验
日志记录
菜单权限
比如导出 Excel 时,可以把属性上的中文名作为表头。
四、泛型约束:让通用方法更安全
泛型是 C# 中非常重要的能力。
但是如果泛型不加约束,代码可控性会变差。
普通泛型方法
public T CreateInstance<T>()
{
return Activator.CreateInstance<T>();
}
加泛型约束
public T CreateInstance<T>() where T : new()
{
return new T();
}
示例
public class User
{
public string Name { get; set; }
}
public static T Create<T>() where T : new()
{
return new T();
}
使用:
User user = Create<User>();
user.Name = "王五";
常见泛型约束
where T : class // T 必须是引用类型
where T : struct // T 必须是值类型
where T : new() // T 必须有无参构造函数
where T : BaseClass // T 必须继承某个基类
where T : IInterface // T 必须实现某个接口
好处
泛型约束可以提高代码安全性。
编译期就能限制类型,减少运行时错误。
使用场景
通用仓储
ORM封装
对象工厂
实体转换
接口统一处理
五、委托:把方法当参数传递
委托可以理解为“方法类型”。
它允许我们把方法像变量一样传来传去。
示例
public delegate int CalculateDelegate(int a, int b);
定义方法:
public static int Add(int a, int b)
{
return a + b;
}
public static int Sub(int a, int b)
{
return a - b;
}
使用:
CalculateDelegate calc = Add;
int result = calc(10, 5);
Console.WriteLine(result);
使用 Func 简化
Func<int, int, int> calc = (a, b) => a + b;
int result = calc(10, 5);
使用 Action
Action<string> log = msg =>
{
Console.WriteLine("日志:" + msg);
};
log("程序启动");
好处
委托可以让代码更灵活。
你可以把“执行什么逻辑”交给调用方决定。
使用场景
回调函数
事件机制
策略模式
日志处理
异步任务完成通知
通用业务处理
六、LINQ:让集合查询像 SQL 一样优雅
LINQ 是 C# 非常经典的黑科技。
它可以用非常简洁的方式处理集合。
示例数据
List<User> users = new List<User>
{
new User { Id = 1, Name = "张三", Age = 18 },
new User { Id = 2, Name = "李四", Age = 25 },
new User { Id = 3, Name = "王五", Age = 30 }
};
查询年龄大于 20 的用户
var result = users.Where(x => x.Age > 20).ToList();
排序
var result = users.OrderByDescending(x => x.Age).ToList();
分组
var groups = users.GroupBy(x => x.Age);
foreach (var group in groups)
{
Console.WriteLine("年龄:" + group.Key);
foreach (var user in group)
{
Console.WriteLine(user.Name);
}
}
投影
var names = users.Select(x => x.Name).ToList();
好处
LINQ 可以减少大量循环代码。
以前要写很多 foreach,现在一行就能完成。
使用场景
集合筛选
排序
分组
去重
分页
对象转换
数据统计
七、表达式树:让代码变成“数据结构”
表达式树是 C# 中比较高级的特性。
它可以把代码逻辑保存成一种树形结构,供程序分析。
示例
Expression<Func<User, bool>> exp = x => x.Age > 18;
这里不是普通委托,而是一棵表达式树。
查看表达式内容
Console.WriteLine(exp.Body);
输出类似:
(x.Age > 18)
好处
表达式树最强的地方是:
可以分析代码
可以动态生成SQL
可以动态拼接查询条件
使用场景
Entity Framework
ORM框架
动态查询
规则引擎
搜索条件构造器
比如 EF 中:
db.Users.Where(x => x.Age > 18);
框架之所以能把这段 C# 转成 SQL,就是因为表达式树。
八、dynamic:运行时动态调用成员
C# 是强类型语言,但它也支持动态类型。
示例
dynamic obj = new System.Dynamic.ExpandoObject();
obj.Name = "张三";
obj.Age = 20;
Console.WriteLine(obj.Name);
Console.WriteLine(obj.Age);
好处
dynamic 可以在运行时决定对象结构。
使用场景
JSON动态解析
COM组件调用
Office自动化
第三方接口返回不固定
快速原型开发
比如接口返回结构不固定时,可以临时使用 dynamic。
注意
dynamic 的缺点是:
编译期不检查
运行时报错风险高
IDE智能提示弱
所以它适合灵活场景,不适合核心业务模型滥用。
九、匿名类型:临时对象不用单独建类
有时候我们只是临时组合几个字段,不想专门创建一个类。
这时可以使用匿名类型。
示例
var user = new
{
Id = 1,
Name = "张三",
Age = 20
};
Console.WriteLine(user.Name);
LINQ 中常用
var result = users.Select(x => new
{
用户名 = x.Name,
是否成年 = x.Age >= 18
}).ToList();
好处
匿名类型适合临时数据组合。
使用场景
LINQ投影
接口返回DTO临时组合
报表字段组合
调试输出
不过匿名类型一般只适合方法内部使用,不建议作为公共接口返回类型。
十、yield return:按需返回数据,节省内存
yield return 可以让方法逐个返回数据,而不是一次性把所有数据加载到内存中。
普通写法
public static List<int> GetNumbers()
{
List<int> list = new List<int>();
for (int i = 0; i < 100; i++)
{
list.Add(i);
}
return list;
}
yield 写法
public static IEnumerable<int> GetNumbers()
{
for (int i = 0; i < 100; i++)
{
yield return i;
}
}
使用
foreach (int num in GetNumbers())
{
Console.WriteLine(num);
}
好处
yield return 不会一次性生成完整集合,而是遍历到哪里生成到哪里。
适合处理大量数据。
使用场景
大数据遍历
文件逐行读取
分页数据生成
流式处理
懒加载
十一、using 自动释放资源
很多资源必须手动释放,比如:
数据库连接
文件流
网络流
图片对象
SqlDataReader
错误写法
FileStream fs = new FileStream("test.txt", FileMode.Open);
StreamReader sr = new StreamReader(fs);
string text = sr.ReadToEnd();
sr.Close();
fs.Close();
如果中途异常,资源可能释放不了。
正确写法
using (FileStream fs = new FileStream("test.txt", FileMode.Open))
using (StreamReader sr = new StreamReader(fs))
{
string text = sr.ReadToEnd();
}
好处
using 可以自动调用 Dispose(),即使中途异常也能释放资源。
使用场景
数据库操作
文件操作
GDI+绘图
网络请求
压缩流
十二、nameof:避免硬编码字符串
错误写法
throw new ArgumentNullException("userName");
如果参数名改了,这个字符串不会自动改。
正确写法
throw new ArgumentNullException(nameof(userName));
示例
public void Login(string userName)
{
if (string.IsNullOrEmpty(userName))
{
throw new ArgumentNullException(nameof(userName));
}
}
好处
nameof 会在编译期获取变量、属性、类的名称。
重构时更安全。
使用场景
异常参数名
日志字段
属性变更通知
反射字段名
校验提示
十三、CallerMemberName:自动获取调用者方法名
有时候写日志时,希望知道是谁调用了当前方法。
示例
public static void Log(string message,
[System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
{
Console.WriteLine($"[{memberName}] {message}");
}
使用:
public void Save()
{
Log("开始保存数据");
}
输出:
[Save] 开始保存数据
好处
不用手动写方法名。
如果方法改名,日志也会自动变化。
使用场景
日志系统
调试追踪
属性变更通知
异常定位
十四、StackTrace:获取程序调用链
StackTrace 可以获取当前代码的调用堆栈。
示例
public static void PrintStack()
{
StackTrace stackTrace = new StackTrace(true);
foreach (StackFrame frame in stackTrace.GetFrames())
{
MethodBase method = frame.GetMethod();
Console.WriteLine(method.DeclaringType.FullName + "." + method.Name);
Console.WriteLine("行号:" + frame.GetFileLineNumber());
}
}
好处
可以知道代码是从哪里调用过来的。
使用场景
日志追踪
异常分析
框架调试
调用链分析
复杂业务排查
不过这个功能不要在高频业务代码中大量使用,因为它有一定性能开销。
十五、异步 async/await:不阻塞线程
示例
public async Task<string> GetDataAsync()
{
using (HttpClient client = new HttpClient())
{
string result = await client.GetStringAsync("https://example.com");
return result;
}
}
WinForms 中使用:
private async void btnLoad_Click(object sender, EventArgs e)
{
btnLoad.Enabled = false;
string result = await GetDataAsync();
txtResult.Text = result;
btnLoad.Enabled = true;
}
好处
async/await 可以让异步代码写起来像同步代码一样清晰。
使用场景
网络请求
数据库访问
文件读写
耗时任务
WinForms防止界面卡死
Web接口并发处理
注意:
async void
除了事件处理器,不建议使用。
普通异步方法应该返回:
Task
Task<T>
十六、并行处理 Parallel.ForEach
如果有一批互不依赖的任务,可以用并行处理提高效率。
示例
List<int> list = Enumerable.Range(1, 100).ToList();
Parallel.ForEach(list, item =>
{
Console.WriteLine("处理:" + item);
});
控制最大并发数
ParallelOptions options = new ParallelOptions
{
MaxDegreeOfParallelism = 4
};
Parallel.ForEach(list, options, item =>
{
Console.WriteLine("处理:" + item);
});
好处
可以充分利用多核 CPU。
使用场景
批量图片处理
批量文件处理
数据计算
日志分析
批量接口调用
注意:
如果内部操作 UI 控件、共享变量、数据库连接,要特别小心线程安全问题。
十七、Lazy:延迟初始化对象
有些对象创建成本比较高,但不一定马上用到。
可以使用 Lazy<T>。
示例
private static Lazy<ConfigManager> _config =
new Lazy<ConfigManager>(() => new ConfigManager());
public static ConfigManager Config
{
get { return _config.Value; }
}
使用
ConfigManager manager = Config;
只有第一次访问 Value 时,才会真正创建对象。
好处
节省资源
避免无意义初始化
默认线程安全
适合单例
使用场景
配置管理器
缓存对象
数据库连接工厂
日志对象
大对象初始化
十八、ConcurrentDictionary:线程安全字典
多线程环境下不能随便使用普通 Dictionary。
错误写法
Dictionary<string, int> dict = new Dictionary<string, int>();
Parallel.For(0, 1000, i =>
{
dict[i.ToString()] = i;
});
这可能导致异常或者数据错乱。
正确写法
ConcurrentDictionary<string, int> dict =
new ConcurrentDictionary<string, int>();
Parallel.For(0, 1000, i =>
{
dict.TryAdd(i.ToString(), i);
});
获取或添加
int value = dict.GetOrAdd("count", 1);
好处
适合多线程读写共享数据。
使用场景
缓存
在线用户列表
任务状态表
接口调用计数
多线程结果收集
十九、MemoryCache:本地缓存神器
如果某些数据不需要每次都查数据库,可以放入缓存。
示例
ObjectCache cache = MemoryCache.Default;
string key = "UserList";
var data = cache[key] as List<User>;
if (data == null)
{
data = GetUsersFromDatabase();
CacheItemPolicy policy = new CacheItemPolicy
{
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(10)
};
cache.Set(key, data, policy);
}
好处
减少数据库访问,提高系统响应速度。
使用场景
菜单缓存
权限缓存
配置缓存
字典表缓存
基础资料缓存
注意:
缓存不是数据库,不能把强一致性要求很高的数据随便缓存。
二十、表达式动态拼接查询条件
很多后台管理系统都有动态查询条件。
例如:
姓名
手机号
开始时间
结束时间
状态
条件可能有,也可能没有。
普通写法容易变成一堆 if
var query = users.AsQueryable();
if (!string.IsNullOrEmpty(name))
{
query = query.Where(x => x.Name.Contains(name));
}
if (age > 0)
{
query = query.Where(x => x.Age == age);
}
这种写法本身没问题,但复杂条件多了之后会越来越难维护。
可以结合表达式树封装动态条件。
简单示例
Expression<Func<User, bool>> condition = x => true;
if (!string.IsNullOrEmpty(name))
{
condition = x => x.Name.Contains(name);
}
var result = users.AsQueryable().Where(condition).ToList();
更复杂的场景可以封装 Expression 合并器。
好处
动态查询能力更强,适合封装通用查询框架。
使用场景
后台列表查询
高级搜索
报表筛选
ORM动态SQL
权限数据过滤
二十一、反射 + 特性实现 Excel 表头映射
这是一个非常实用的组合技巧。
定义特性
[AttributeUsage(AttributeTargets.Property)]
public class ExcelColumnAttribute : Attribute
{
public string Title { get; private set; }
public ExcelColumnAttribute(string title)
{
Title = title;
}
}
实体类
public class UserExportDto
{
[ExcelColumn("用户编号")]
public int Id { get; set; }
[ExcelColumn("用户姓名")]
public string Name { get; set; }
[ExcelColumn("年龄")]
public int Age { get; set; }
}
读取表头
public static void PrintExcelHeaders<T>()
{
PropertyInfo[] props = typeof(T).GetProperties();
foreach (PropertyInfo prop in props)
{
ExcelColumnAttribute attr =
prop.GetCustomAttributes(typeof(ExcelColumnAttribute), false)
.FirstOrDefault() as ExcelColumnAttribute;
if (attr != null)
{
Console.WriteLine(attr.Title);
}
}
}
使用
PrintExcelHeaders<UserExportDto>();
好处
以后导出字段中文名不需要写死在代码中。
实体属性上怎么标,导出时就怎么显示。
这就是很多框架底层常用的思想。
二十二、反射调用方法:插件式开发基础
假设有一个类:
public class ReportService
{
public void Export()
{
Console.WriteLine("开始导出报表");
}
}
可以通过反射调用方法:
Type type = typeof(ReportService);
object instance = Activator.CreateInstance(type);
MethodInfo method = type.GetMethod("Export");
method.Invoke(instance, null);
好处
方法名可以从配置文件、数据库、菜单权限中读取,实现动态调用。
使用场景
插件系统
动态菜单
报表系统
工作流节点
规则引擎
自动任务调度
例如数据库配置:
任务名称:导出报表
类名:ReportService
方法名:Export
系统运行时根据配置动态执行对应方法。
二十三、事件 Event:对象之间解耦通信
事件是 C# 非常重要的机制。
WinForms 里的按钮点击就是事件。
button1.Click += button1_Click;
我们也可以自定义事件。
示例
public class DownloadService
{
public event Action<int> ProgressChanged;
public void Start()
{
for (int i = 0; i <= 100; i += 10)
{
ProgressChanged?.Invoke(i);
Thread.Sleep(100);
}
}
}
使用:
DownloadService service = new DownloadService();
service.ProgressChanged += progress =>
{
Console.WriteLine("下载进度:" + progress + "%");
};
service.Start();
好处
事件可以让对象之间解耦。
下载服务只负责通知进度,不关心界面怎么显示。
使用场景
进度通知
状态变化
模块通信
WinForms控件交互
任务完成回调
二十四、反射实现对象属性复制
很多项目中经常需要把一个对象的属性复制到另一个对象。
比如:
Entity 转 DTO
DTO 转 ViewModel
表单对象转业务对象
示例
public static void CopyProperties(object source, object target)
{
if (source == null || target == null)
{
return;
}
PropertyInfo[] sourceProps = source.GetType().GetProperties();
PropertyInfo[] targetProps = target.GetType().GetProperties();
foreach (PropertyInfo sourceProp in sourceProps)
{
PropertyInfo targetProp = targetProps.FirstOrDefault(p =>
p.Name == sourceProp.Name &&
p.PropertyType == sourceProp.PropertyType &&
p.CanWrite);
if (targetProp != null)
{
object value = sourceProp.GetValue(source, null);
targetProp.SetValue(target, value, null);
}
}
}
使用
User user = new User
{
Id = 1,
Name = "张三",
Age = 20
};
UserDto dto = new UserDto();
CopyProperties(user, dto);
好处
减少大量重复赋值代码。
以前可能要写:
dto.Id = user.Id;
dto.Name = user.Name;
dto.Age = user.Age;
字段多的时候非常麻烦。
注意
反射有性能开销。
如果是高频调用场景,可以使用缓存、表达式树或 AutoMapper。
二十五、总结:黑科技不是炫技,而是解决复杂问题
C# 的“黑科技”很多,但真正重要的不是会不会写,而是知道什么时候该用。
这些技术的共同价值:
减少重复代码
提升代码复用性
增强系统扩展性
降低模块耦合
提升开发效率
让项目更容易维护
但是也要注意:
反射不要滥用
dynamic 不要滥用
多线程一定注意线程安全
异步方法不要乱写 async void
缓存要注意数据一致性
真正成熟的程序员,不是写最复杂的代码,而是能用合适的技术解决合适的问题。
更多推荐



所有评论(0)