本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C#是由微软推出的高效、类型安全的编程语言,广泛应用于Windows开发、Web及游戏领域。本压缩包汇集作者多年开发经验沉淀的C#公共类库,包含120多个封装良好的通用类,涵盖数据处理、网络通信、文件IO、并发编程、加密解密、日志记录等多个核心模块。该类库具备高复用性、良好扩展性和清晰的面向对象设计,可显著提升开发效率与代码质量。通过导入命名空间即可快速调用,适用于各类.NET项目,是C#开发者值得拥有的实用工具集。

C#类库的设计艺术:从理论到企业级实战

在现代软件开发中,我们常常面临这样一个矛盾:业务需求越来越复杂,交付周期却不断压缩。一个刚接手项目的开发者,面对几十万行代码时,最常问的问题往往是——“这个功能到底是在哪儿实现的?” 🤔 更糟糕的是,当他好不容易找到相关代码,却发现同样的逻辑在三个不同的地方被重复实现了,而且每一份都略有差异。

这正是我第一次参与大型项目维护时的真实写照。那时候我才明白, 真正决定系统寿命的,从来不是某个炫酷的新技术,而是那些默默支撑着整个架构的公共组件 。而这些组件的核心载体,就是我们今天要深入探讨的主题——C#公共类库。

但请注意,这里的“类库”绝不是简单地把常用方法打包成DLL就完事了。它是一套经过千锤百炼的工程哲学,是团队智慧的结晶,更是技术债的防火墙。想象一下,如果你能在每个新项目中都复用一套经过生产环境验证的数据访问层、线程调度器和网络通信模块,那该节省多少时间?又会避免多少潜在的坑?

所以,让我们抛开那些教科书式的定义,从一个更接地气的角度来重新认识C#类库的价值。


说到类库,很多人第一反应就是“代码复用”。没错,这是最基本的功能,但远远不够。真正的高手看类库,看到的是三个维度:

  1. 效率维度 :减少重复劳动,让80%的基础工作变成“搭积木”;
  2. 质量维度 :把曾经踩过的坑固化成防御性代码,防止新人再掉进去;
  3. 协作维度 :统一编码风格和技术栈,让五个人写的代码看起来像一个人写的。

举个例子,你有没有遇到过这样的场景:团队里有人喜欢用 var ,有人坚持写全类型名;有人异常处理直接 try-catch(Exception) ,有人则精细区分具体异常类型……这些问题看似不大,但日积月累就会形成巨大的沟通成本。而一个好的类库,就像一本《编码规范》,但它不是靠文档约束人,而是通过API设计引导行为。

比如我们在数据访问层封装了一个 SafeExecute<T> 方法,内部自动处理连接打开、事务管理、异常记录和资源释放。这时候新人哪怕什么都不懂,只要调这个方法就能写出相对安全的数据库操作代码。久而久之,大家的习惯也就慢慢统一了。

这就是所谓的“ 通过设计达成共识 ”。


那么,如何打造这样一套既能提升效率又能保障质量的类库呢?核心就在于四个字: 高内聚、低耦合

听起来是不是很耳熟?但这六个字背后藏着太多细节。以我们最常见的数据库操作为例,很多人一开始的做法是这样的:

// ❌ 反模式:硬编码 + 紧耦合
public class UserService
{
    private string _connStr = "Server=...;Database=...";

    public User GetUser(int id)
    {
        using (var conn = new SqlConnection(_connStr))
        {
            conn.Open();
            // 执行查询...
        }
    }
}

问题在哪?一目了然:连接字符串写死、依赖具体数据库驱动、没有异常处理、无法测试。更重要的是,一旦将来要换成MySQL或者加个读写分离,就得改遍所有类似代码。

正确的做法应该是怎样的?🤔

首先,我们要把“获取数据库连接”这件事抽象出来:

public interface IDataAccessProvider
{
    IDbConnection GetOpenConnection();
    Task<IDbConnection> GetOpenConnectionAsync(CancellationToken ct = default);
}

看到了吗?这里只依赖 .NET 自带的 IDbConnection 接口,而不是具体的 SqlConnection 。这意味着什么?意味着你可以轻松实现一个 MySqlDataAccessProvider 或者 PostgreSqlDataAccessProvider ,然后通过配置切换,完全不影响上层业务逻辑。

public class SqlServerDataAccessProvider : IDataAccessProvider
{
    private readonly string _connectionString;

    public SqlServerDataAccessProvider(string connectionString)
    {
        _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
    }

    public IDbConnection GetOpenConnection()
    {
        var connection = new SqlConnection(_connectionString);
        connection.Open();
        return connection;
    }

    // 异步版本略
}

关键点来了:为什么每次都要创建新的连接,而不是共享一个静态连接?🚨 因为 SqlConnection 不是线程安全的!多个线程同时使用同一个连接对象会导致不可预知的行为。所以最佳实践是“ 每个操作独占连接 ”,用完即关,交给连接池去复用底层物理连接。

说到这里,不得不提一下 .NET 的连接池机制。默认情况下,SQL Server 的 SqlConnection 会自动启用连接池。当你调用 Close() 时,并不会真正断开与数据库的物理连接,而是把它放回池子里等待下次复用。这样一来,既避免了频繁建立TCP连接的开销,又保证了应用层面的资源隔离。

不过,在高并发系统中,你还得对连接池进行精细化控制。来看几个关键参数:

配置项 建议值 说明
Max Pool Size 200–500 控制最大并发连接数,防止单个应用耗尽数据库连接资源
Min Pool Size 5–10 初始化一定数量的连接,减少冷启动延迟
Connection Timeout 15–30秒 连接超时时间,避免请求堆积
Command Timeout 按需设置 查询执行超时,防止慢SQL拖垮服务

示例连接字符串:

Server=localhost;Database=AppDb;User Id=sa;Password=Pass123;
Max Pool Size=300;Min Pool Size=10;Connection Timeout=30;Pooling=true;

记住一句口诀: 连接要短命,池子要管住

此外,为了防止连接泄露(忘了 Dispose ),一定要坚持使用 using 语句:

public async Task<int> GetUserCountAsync(CancellationToken ct)
{
    using var conn = await _provider.GetOpenConnectionAsync(ct);
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "SELECT COUNT(*) FROM Users";
    cmd.CommandTimeout = 60;
    return (int)await cmd.ExecuteScalarAsync(ct);
}

即使发生异常, using 也会确保连接被正确释放。这一点看似简单,但在真实项目中却是最容易出问题的地方之一。


聊完数据层,咱们再来看看并发处理这块“硬骨头”。🔥

多线程编程向来是 bug 的重灾区。死锁、竞态条件、内存泄漏……随便哪个都能让你加班到凌晨。而在 C# 中,虽然有 async/await 这种神器,但如果理解不深,反而更容易写出隐蔽的问题。

先来个小测验:下面三种方式有什么区别?

// A: Thread
new Thread(() => { /* 工作 */ }).Start();

// B: ThreadPool
ThreadPool.QueueUserWorkItem(_ => { /* 工作 */ });

// C: Task
Task.Run(() => { /* 工作 */ });

如果你的回答是“都能开启后台任务”,那还不够精准。它们的本质区别在于:

  • Thread :对应操作系统线程,开销大,适合长时间运行的任务;
  • ThreadPool :利用CLR线程池,轻量级,适合短期任务;
  • Task :基于状态机的异步模型,支持 await ,能更好地与异步I/O集成。

所以在实际项目中,我们的建议非常明确: 除非你真的需要长期独占一个OS线程(比如监控服务),否则一律优先使用 Task.Run

但事情还没完。默认的 TaskScheduler 是全局共享的,所有任务平等地竞争线程池资源。这就带来一个问题:如果某些CPU密集型任务把线程池占满了,I/O密集型任务(如数据库查询、HTTP调用)就会“饿死”。

怎么办?答案是自定义任务调度器!

我们设计了一个 AsyncTaskScheduler 基类,允许你控制最大并行度、任务优先级等策略:

public abstract class AsyncTaskScheduler : TaskScheduler
{
    protected readonly ConcurrentQueue<Task> _taskQueue;
    protected readonly int _maxDegreeOfParallelism;

    public AsyncTaskScheduler(int maxDegreeOfParallelism = 4)
    {
        _taskQueue = new ConcurrentQueue<Task>();
        _maxDegreeOfParallelism = maxDegreeOfParallelism;
    }

    protected override void QueueTask(Task task)
    {
        _taskQueue.Enqueue(task);
        NotifyNewWork(); // 触发工作线程拉取任务
    }

    protected abstract void NotifyNewWork();

    // 其他抽象方法略
}

然后可以基于信号量实现一个固定并发的调度器:

public class SemaphoreTaskScheduler : AsyncTaskScheduler
{
    private readonly SemaphoreSlim _semaphore;
    private readonly List<Thread> _workerThreads;

    public SemaphoreTaskScheduler(int maxDegreeOfParallelism) : base(maxDegreeOfParallelism)
    {
        _semaphore = new SemaphoreSlim(maxDegreeOfParallelism, maxDegreeOfParallelism);
        _workerThreads = new List<Thread>(maxDegreeOfParallelism);
        for (int i = 0; i < maxDegreeOfParallelism; i++)
        {
            var thread = new Thread(WorkerLoop) { IsBackground = true };
            _workerThreads.Add(thread);
            thread.Start();
        }
    }

    protected override void NotifyNewWork() => _semaphore.Release();

    private async void WorkerLoop()
    {
        while (true)
        {
            await _semaphore.WaitAsync();
            if (_taskQueue.TryDequeue(out var task))
            {
                TryExecuteTask(task);
            }
        }
    }
}

这样就可以做到:最多只有两个任务并行执行,其余排队等待。非常适合用于限制对第三方API的调用频率,或者保护本地资源不被耗尽。

应用场景举例:假设你要批量处理1000个文件上传,但服务器只能承受同时处理5个上传请求。这时候只要把任务扔给 SemaphoreTaskScheduler(5) ,就能自动实现限流,无需额外编写复杂的协调逻辑。

是不是感觉一下子轻松了很多?😎


再说说 async/await 使用中的几个坑,很多人都栽过跟头。

第一个是上下文捕获问题。在 WinForms 或 WPF 中, await 会自动捕获当前的 SynchronizationContext ,确保后续代码回到UI线程执行。这本来是个贴心的设计,但在类库代码中却可能成为性能瓶颈。

为什么?因为每一次 await 都要调度回原始上下文,增加了不必要的开销。更严重的是,如果那个上下文已经关闭(比如窗体被销毁了),程序就会卡住甚至崩溃。

解决方案很简单:在类库中使用 .ConfigureAwait(false) 显式禁用上下文捕获:

public async Task ProcessAsync()
{
    await GetDataFromDbAsync().ConfigureAwait(false);
    await SendToApiAsync().ConfigureAwait(false);
}

这条规则应该作为团队的强制编码规范。记住一句话: 类库不知道调用方是谁,所以不要假设上下文存在

第二个问题是 ValueTask 的误用。很多人看到它可以减少GC压力,就迫不及待地 everywhere 替换 Task 。但你得知道它的适用场景: 当方法大概率能同步完成时才用

典型例子是缓存读取:

public ValueTask<string> ReadAsStringAsync(bool useCache = true)
{
    if (useCache && _cache.TryGetValue("key", out var cached))
        return new ValueTask<string>(cached); // 同步返回,无Task分配

    return new ValueTask<string>(LoadFromDiskAsync());
}

但如果方法总是异步的,强行用 ValueTask 反而会造成更多堆分配(因为内部要用 Task 包装)。而且 ValueTask 只能 await 一次,重复使用会抛异常。所以务必谨慎。


接下来聊聊网络通信,这部分可以说是现代应用的“生命线”。

提到HTTP调用,很多人第一反应就是 HttpClient 。但你知道吗?直接 new HttpClient() 在高并发下会导致端口耗尽!原因是每次实例化都会创建新的Socket连接,而连接关闭后进入TIME_WAIT状态的时间很长(通常几分钟),期间端口不能复用。

那能不能做成单例?也不行!静态 HttpClient 不会刷新DNS,一旦后端IP变了,请求就会失败。

正确的解法是什么?ASP.NET Core 给我们提供了标准答案: IHttpClientFactory

services.AddHttpClient<IRemoteCommandService, RemoteCommandService>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
})
.SetHandlerLifetime(TimeSpan.FromMinutes(5)) // 5分钟后刷新DNS
.AddHttpMessageHandler<LoggingHandler>()
.AddHttpMessageHandler<AuthenticationHandler>();

它的原理是池化 HttpMessageHandler 实例。每次获取 HttpClient 时,工厂从池子里拿一个活跃的handler复用连接,同时定期淘汰旧的handler以应对IP变更。完美解决了前面两个痛点。

更妙的是,它还支持责任链模式的拦截器( DelegatingHandler )。我们可以把日志、认证、重试等横切关注点抽出来单独实现:

public class LoggingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var startTime = DateTimeOffset.UtcNow;
        _logger.LogInformation("发起请求: {Method} {Uri}", request.Method, request.RequestUri);

        try
        {
            var response = await base.SendAsync(request, cancellationToken);
            var duration = DateTimeOffset.UtcNow - startTime;
            _logger.LogInformation("收到响应: {StatusCode} ({Duration}ms)", 
                response.StatusCode, duration.TotalMilliseconds);
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "请求失败: {Method} {Uri}", request.Method, request.RequestUri);
            throw;
        }
    }
}
public class AuthenticationHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var token = await _tokenProvider.GetValidTokenAsync(cancellationToken);
        request.Headers.Authorization = new("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }
}

这些handler按注册顺序形成一条管道,请求依次经过日志→认证→发送,响应则反向回传。清晰、灵活、可复用。

当然,光有正常流程还不够。现实世界充满了不确定性:网络抖动、服务宕机、临时故障……所以我们必须加入弹性机制。

这里强烈推荐结合 Polly 库实现智能重试:

services.AddHttpClient<IRemoteCommandService, RemoteCommandService>()
    .AddPolicyHandler(Policy
        .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
        .Or<HttpRequestException>()
        .WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(Math.Pow(2, i)), // 指数退避
            onRetry: (outcome, timespan, retryCount, context) =>
            {
                _logger.LogWarning($"第 {retryCount} 次重试,等待 {timespan.TotalSeconds}s");
            }));

指数退避策略(2s, 4s, 8s…)比固定间隔更科学,能有效缓解下游服务的压力。配合日志输出,还能方便排查问题。


对于需要长连接的场景,比如实时消息推送或设备指令下发,TCP 仍然是不可替代的选择。但它带来的挑战也更多:粘包、拆包、心跳保活、断线重连……

其中最让人头疼的就是 粘包问题 ——TCP是字节流协议,不保证消息边界。你发送了两条消息 "Hello" "World" ,接收方可能一次性收到 "HelloWorld" ,也可能分三次收到 "Hel" , "loW" , "orld"

解决办法很简单粗暴:加长度头。也就是在每条消息前面加上4字节的长度字段。

public async Task<byte[]> ReceiveMessageAsync()
{
    // 先读4字节长度
    var header = new byte[4];
    await ReceiveExactAsync(header, 0, 4);
    var bodyLength = BitConverter.ToInt32(header, 0);

    // 再根据长度读正文
    var body = new byte[bodyLength];
    await ReceiveExactAsync(body, 0, bodyLength);

    return body;
}

private async Task<int> ReceiveExactAsync(byte[] buffer, int offset, int count)
{
    var totalReceived = 0;
    while (totalReceived < count)
    {
        var received = await _socket.ReceiveAsync(
            new ArraySegment<byte>(buffer, offset + totalReceived, count - totalReceived),
            SocketFlags.None);
        if (received == 0) throw new IOException("连接已关闭");
        totalReceived += received;
    }
    return totalReceived;
}

这个模式虽然简单,但极其有效。几乎所有成熟的通信协议(如WebSocket、gRPC)都在用类似的机制。

至于心跳检测,核心思想是“主动出击”。别指望操作系统能及时发现断网,防火墙可能十几分钟才清理一次空闲连接。所以我们得自己定时发PING包:

public class HeartbeatMonitor
{
    private Timer _timer;
    private DateTime _lastActivityTime = DateTime.UtcNow;

    public void Start(int intervalMs = 30000)
    {
        _timer = new Timer(async _ => await CheckHeartbeat(), null,
            TimeSpan.Zero, TimeSpan.FromMilliseconds(intervalMs));
    }

    private async Task CheckHeartbeat()
    {
        if (DateTime.UtcNow - _lastActivityTime > TimeSpan.FromSeconds(60))
        {
            try
            {
                await _session.PingAsync();
                _lastActivityTime = DateTime.UtcNow;
            }
            catch
            {
                _session.OnConnectionLost();
            }
        }
    }

    public void RecordActivity() => _lastActivityTime = DateTime.UtcNow;
}

配合自动重连:

public async Task ReconnectAsync(string host, int port, int maxRetries = 5)
{
    for (int i = 0; i < maxRetries; i++)
    {
        try
        {
            await _socket.ConnectAsync(host, port);
            OnConnected();
            return;
        }
        catch when (i < maxRetries - 1)
        {
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); // 指数退避
        }
    }
    throw new ConnectionException("重连失败");
}

这一套组合拳下来,客户端即便遭遇短暂断网也能快速恢复,用户体验几乎不受影响。


最后谈谈I/O操作,尤其是大文件处理这种容易OOM的场景。

很多人习惯性地用 File.ReadAllBytes() ,殊不知一旦碰到几百MB的日志文件,程序瞬间就崩了。正道是 流式处理 ——边读边处理,始终保持小内存占用。

比如计算大文件SHA256哈希:

public static async Task<string> ComputeSha256HashAsync(string filePath, IProgress<double> progress = null)
{
    using var sha256 = SHA256.Create();
    using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 65536, true);

    long totalBytes = stream.Length;
    long bytesRead = 0;
    var buffer = new byte[65536];

    int read;
    while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
    {
        sha256.TransformBlock(buffer, 0, read, null, 0);
        bytesRead += read;
        progress?.Report((double)bytesRead / totalBytes * 100);
    }

    sha256.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
    return BitConverter.ToString(sha256.Hash).Replace("-", "").ToLowerInvariant();
}

关键点:
- 缓冲区大小设为64KB,平衡I/O效率和内存使用;
- 使用 TransformBlock 增量更新哈希状态;
- 支持进度报告,可用于UI显示;
- 整个过程峰值内存不超过100KB。

再进一步,还可以结合 Span<T> 优化性能:

while ((read = await stream.ReadAsync(buffer)) > 0)
{
    ReadOnlySpan<byte> chunk = buffer.AsSpan(0, read);
    sha256.AppendData(chunk); // 假设有扩展方法
}

Span<T> 能避免不必要的数组拷贝,在高频小块读取时效果显著。

对于需要压缩归档的场景,更要善用流管道技术:

public static byte[] SerializeAndCompress<T>(T data)
{
    using var output = new MemoryStream();
    using var compression = new DeflateStream(output, CompressionLevel.Optimal);
    using var writer = new StreamWriter(compression);

    var serializer = new JsonSerializer();
    serializer.Serialize(writer, data);
    writer.Flush();
    compression.Dispose();
    return output.ToArray();
}

注意最后必须显式 Dispose() 压缩流,否则解压时会报“流意外结束”。这是因为压缩算法需要写入尾部标记才能完整。


说了这么多,你可能会问:这套类库到底该怎么组织和发布?

我的建议是: 按功能垂直划分命名空间

Company.Library.Data
Company.Library.Threading
Company.Library.Network
Company.Library.Security
Company.Library.Logging

每个命名空间高度内聚,职责单一。比如 Data 里面全是数据库相关工具, Security 负责加密解密、JWT生成等。这样别人一看就知道该去哪儿找自己需要的功能。

发布方式首选 NuGet 包,走 CI/CD 流水线自动化构建和推送到私有源。版本号严格遵循 SemVer 规范:

  • 1.0.0 → 初始稳定版
  • 1.1.0 → 新增功能(兼容)
  • 1.1.1 → Bug修复
  • 2.0.0

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C#是由微软推出的高效、类型安全的编程语言,广泛应用于Windows开发、Web及游戏领域。本压缩包汇集作者多年开发经验沉淀的C#公共类库,包含120多个封装良好的通用类,涵盖数据处理、网络通信、文件IO、并发编程、加密解密、日志记录等多个核心模块。该类库具备高复用性、良好扩展性和清晰的面向对象设计,可显著提升开发效率与代码质量。通过导入命名空间即可快速调用,适用于各类.NET项目,是C#开发者值得拥有的实用工具集。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐