前言

很多人写 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
缓存要注意数据一致性

真正成熟的程序员,不是写最复杂的代码,而是能用合适的技术解决合适的问题。

更多推荐