C# IO和序列化

1. 文件 IO (File IO)

基础的文件读写使用 System.IO 命名空间下的静态类 File 或流式操作 StreamReader/StreamWriter。对于文本文件,File.ReadAllText 和 File.WriteAllText 最为便捷;对于需要控制编码或大文件流式处理,应使用 Stream。

步骤‌:

  • 定义文件路径。
  • 使用 using 语句确保流在使用后自动关闭和释放。
  • 执行写入或读取操作。
  • 处理可能的异常(如文件不存在、权限不足)。
cs 复制代码
using System;
using System.IO;
using System.Text;

public class FileIoExample
{
    public static void Run()
    {
        string filePath = "sample.txt";
        string content = "Hello, .NET Core IO!";

        // 1. 同步写入文件
        // File.WriteAllText 会自动处理文件的创建、写入和关闭
        File.WriteAllText(filePath, content, Encoding.UTF8);
        Console.WriteLine("文件写入完成。");

        // 2. 同步读取文件
        if (File.Exists(filePath))
        {
            string readContent = File.ReadAllText(filePath, Encoding.UTF8);
            Console.WriteLine($"读取内容: {readContent}");
        }

        // 3. 使用 Stream 进行更底层的控制(例如追加内容)
        using (StreamWriter sw = new StreamWriter(filePath, true, Encoding.UTF8)) // true 表示追加
        {
            sw.WriteLine("\n这是追加的一行。");
        }
        
        Console.WriteLine("追加写入完成。");
    }
}

2. 超大文件处理 (Large File Processing)

处理 GB 级别的大文件时,严禁一次性将文件加载到内存(如 File.ReadAllBytes),这会导致 OutOfMemoryException。正确做法是使用 FileStream 配合缓冲区进行分块读取(Chunked Reading)。

步骤‌:

  • 打开 FileStream。
  • 定义一个固定大小的字节数组作为缓冲区(如 4KB 或 8KB)。
  • 循环调用 Read 方法,每次读取一块数据。
  • 在循环中处理每一块数据(如计算哈希、写入网络流等)。
  • 直到 Read 返回 0,表示文件结束。
cs 复制代码
using System;
using System.IO;
using System.Threading.Tasks;

public class LargeFileExample
{
    public static async Task ProcessLargeFileAsync(string sourcePath, string destPath)
    {
        const int bufferSize = 8192; // 8KB 缓冲区
        byte[] buffer = new byte[bufferSize];

        // 使用异步流操作以提高吞吐量,避免阻塞线程
        using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync: true))
        using (FileStream destStream = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, useAsync: true))
        {
            int bytesRead;
            long totalBytesRead = 0;

            while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
            {
                // 写入目标文件
                await destStream.WriteAsync(buffer, 0, bytesRead);
                totalBytesRead += bytesRead;
                
                // 模拟进度报告
                if (totalBytesRead % (1024 * 1024) == 0) // 每处理 1MB 报告一次
                {
                    Console.WriteLine($"已处理: {totalBytesRead / (1024 * 1024)} MB");
                }
            }
        }
        Console.WriteLine("大文件复制完成。");
    }
}

3. 序列化反序列化 (Serialization/Deserialization)

.NET Core 推荐优先使用 System.Text.Json 进行 JSON 序列化,因为它性能高且无需额外依赖。对于二进制序列化,BinaryFormatter 已被标记为过时且不安全,建议使用 MessagePack 或 Protobuf,但此处展示标准的 JSON 序列化作为通用示例。

步骤‌:

  • 定义数据模型类。
  • 使用 JsonSerializer.Serialize 将对象转换为 JSON 字符串。
  • 使用 JsonSerializer.Deserialize<T> 将 JSON 字符串还原为对象。
  • 配置 JsonSerializerOptions 以自定义行为(如驼峰命名、忽略空值)。
cs 复制代码
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    public DateTime RegisterDate { get; set; }
}

public class SerializationExample
{
    public static void Run()
    {
        var user = new User
        {
            Name = "Alice",
            Age = 30,
            RegisterDate = DateTime.UtcNow
        };

        // 1. 序列化
        var options = new JsonSerializerOptions
        {
            WriteIndented = true, // 格式化输出
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase // 驼峰命名
        };
        
        string jsonString = JsonSerializer.Serialize(user, options);
        Console.WriteLine("序列化结果:");
        Console.WriteLine(jsonString);

        // 2. 反序列化
        User deserializedUser = JsonSerializer.Deserialize<User>(jsonString, options);
        Console.WriteLine($"\n反序列化结果: Name={deserializedUser.Name}, Age={deserializedUser.Age}");
    }
}

4. XML/JSON 处理

虽然 JSON 是主流,但在遗留系统或特定配置场景中仍需处理 XML。.NET Core 中处理 XML 推荐使用 System.Xml.Linq (XDocument),它比传统的 XmlDocument 更简洁且支持 LINQ 查询。

步骤‌:

‌JSON ‌: 同上节,使用 System.Text.Json。

XML:

  • 创建 XDocument 或 XElement 构建 XML 树。
  • 使用 Save 方法写入文件或字符串。
  • 使用 Load 或 Parse 读取 XML。
  • 使用 LINQ to XML 查询节点。
cs 复制代码
using System;
using System.Xml.Linq;
using System.Text.Json;

public class XmlJsonExample
{
    public static void Run()
    {
        // --- JSON 部分 (简略,参考上一节) ---
        var data = new { Id = 1, Value = "Test" };
        string json = JsonSerializer.Serialize(data);
        Console.WriteLine($"JSON: {json}");

        // --- XML 部分 ---
        
        // 1. 创建 XML
        XDocument doc = new XDocument(
            new XElement("Users",
                new XElement("User",
                    new XAttribute("Id", 1),
                    new XElement("Name", "Bob"),
                    new XElement("Age", 25)
                ),
                new XElement("User",
                    new XAttribute("Id", 2),
                    new XElement("Name", "Charlie"),
                    new XElement("Age", 35)
                )
            )
        );

        string xmlString = doc.ToString();
        Console.WriteLine("\nXML 结构:");
        Console.WriteLine(xmlString);

        // 2. 解析 XML
        XDocument parsedDoc = XDocument.Parse(xmlString);
        
        // 使用 LINQ 查询所有用户姓名
        var names = from user in parsedDoc.Descendants("User")
                    select user.Element("Name")?.Value;

        Console.WriteLine("\n提取的用户名:");
        foreach (var name in names)
        {
            Console.WriteLine(name);
        }
    }
}

5. 图片缩放 (Image Resizing)

NET Core 跨平台图像处理不再支持 System.Drawing (GDI+),推荐使用 ‌SkiaSharp ‌ 或 ‌ImageSharp ‌。以下示例使用 ‌SkiaSharp‌,它是 Google Skia 图形引擎的 .NET 绑定,性能优异且跨平台。

‌前置要求 ‌:

需安装 NuGet 包:SkiaSharp 和 SkiaSharp.NativeAssets.Linux.NoDependencies (如果在 Linux Docker 环境中)。

步骤‌:

  • 加载原始图片为 SKBitmap。
  • 计算缩放后的宽高,保持纵横比。
  • 使用 Resize 方法创建新的位图,指定高质量过滤模式(如 Lanczos3)。
  • 将新位图编码为 PNG 或 JPEG 并保存。
cs 复制代码
using System;
using System.IO;
using SkiaSharp;

public class ImageResizeExample
{
    public static void ResizeImage(string inputPath, string outputPath, int maxWidth, int maxHeight)
    {
        // 1. 解码原始图片
        using (var original = SKBitmap.Decode(inputPath))
        {
            if (original == null)
            {
                throw new Exception("无法解码图片文件。");
            }

            // 2. 计算缩放比例 (保持纵横比)
            float scaleWidth = (float)maxWidth / original.Width;
            float scaleHeight = (float)maxHeight / original.Height;
            float scale = Math.Min(scaleWidth, scaleHeight);
            
            // 如果图片本身小于目标尺寸,则不放大
            if (scale >= 1.0f) 
            {
                File.Copy(inputPath, outputPath, true);
                Console.WriteLine("图片无需缩放,已复制。");
                return;
            }

            int newWidth = (int)(original.Width * scale);
            int newHeight = (int)(original.Height * scale);

            // 3. 执行缩放 (使用 Lanczos3 算法保证质量)
            using (var resized = original.Resize(new SKImageInfo(newWidth, newHeight), SKBitmapResizeMethod.Lanczos3))
            {
                if (resized == null)
                {
                    throw new Exception("图片缩放失败。");
                }

                // 4. 编码并保存
                using (var image = SKImage.FromBitmap(resized))
                using (var data = image.Encode(SKEncodedImageFormat.Jpeg, 80)) // 80% 质量
                using (var stream = File.OpenWrite(outputPath))
                {
                    data.SaveTo(stream);
                }
            }
        }
        Console.WriteLine($"图片已缩放并保存至: {outputPath}");
    }
}

6. 验证码生成 (Captcha Generation)

生成验证码通常涉及绘制随机字符、添加干扰线/噪点。在 .NET Core 中,同样推荐使用 ‌SkiaSharp ‌ 或 ‌SixLabors.ImageSharp ‌。以下示例使用 ‌SkiaSharp‌ 绘制简单的文本验证码。

步骤‌:

  • 生成随机字符串。
  • 创建空白画布 (SKSurface)。
  • 绘制背景色。
  • 绘制干扰线(可选)。
  • 绘制验证码文本,使用随机字体大小或轻微旋转增加识别难度。
  • 输出为字节流或 Base64 字符串供前端显示。
cs 复制代码
using System;
using System.IO;
using SkiaSharp;

public class CaptchaExample
{
    private static readonly Random _random = new Random();
    private const string Chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 去除易混淆字符

    public static byte[] GenerateCaptcha(int width = 120, int height = 40, int codeLength = 4)
    {
        string code = GenerateRandomCode(codeLength);

        // 1. 创建表面和画布
        using (var surface = SKSurface.Create(new SKImageInfo(width, height)))
        {
            var canvas = surface.Canvas;
            canvas.Clear(SKColors.White); // 背景白色

            // 2. 绘制干扰线
            using (var paint = new SKPaint())
            {
                paint.Color = SKColors.LightGray;
                paint.StrokeWidth = 1;
                for (int i = 0; i < 5; i++)
                {
                    float x1 = _random.Next(width);
                    float y1 = _random.Next(height);
                    float x2 = _random.Next(width);
                    float y2 = _random.Next(height);
                    canvas.DrawLine(x1, y1, x2, y2, paint);
                }
            }

            // 3. 绘制验证码文本
            using (var paint = new SKPaint())
            {
                paint.Color = SKColors.Black;
                paint.TextSize = 24;
                paint.IsAntialias = true;
                
                // 简单居中计算
                float textWidth = paint.MeasureText(code);
                float startX = (width - textWidth) / 2;
                float startY = height / 2 + 10; // 基线调整

                canvas.DrawText(code, startX, startY, paint);
            }

            // 4. 编码为 PNG 字节数组
            using (var image = surface.Snapshot())
            using (var data = image.Encode(SKEncodedImageFormat.Png, 100))
            {
                return data.ToArray();
            }
        }
    }

    private static string GenerateRandomCode(int length)
    {
        char[] chars = new char[length];
        for (int i = 0; i < length; i++)
        {
            chars[i] = Chars[_random.Next(Chars.Length)];
        }
        return new string(chars);
    }
}

7. 水印添加 (Watermarking)

水印分为文字水印和图片水印。核心逻辑是将原图绘制到画布底层,然后在顶层绘制半透明的文字或图片。

步骤‌:

  • 加载原图。
  • 创建与原图尺寸相同的画布。
  • 将原图绘制到画布上。
  • 设置画笔透明度(Alpha 通道)。
  • 在指定位置(如右下角)绘制水印文字。
  • 保存结果。
cs 复制代码
using System;
using System.IO;
using SkiaSharp;

public class WatermarkExample
{
    public static void AddTextWatermark(string inputPath, string outputPath, string watermarkText)
    {
        // 1. 加载原图
        using (var original = SKBitmap.Decode(inputPath))
        {
            if (original == null) throw new Exception("无效图片");

            // 2. 创建表面
            using (var surface = SKSurface.Create(new SKImageInfo(original.Width, original.Height)))
            {
                var canvas = surface.Canvas;

                // 3. 绘制原图
                canvas.DrawBitmap(original, 0, 0);

                // 4. 绘制水印文字
                using (var paint = new SKPaint())
                {
                    paint.Color = new SKColor(255, 255, 255, 128); // 白色,50% 透明度
                    paint.TextSize = 40;
                    paint.IsAntialias = true;
                    
                    // 测量文字大小以便定位
                    float textWidth = paint.MeasureText(watermarkText);
                    
                    // 定位在右下角,留边距 20px
                    float x = original.Width - textWidth - 20;
                    float y = original.Height - 20;

                    // 可选:添加阴影效果使水印更清晰
                    paint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2);
                    
                    canvas.DrawText(watermarkText, x, y, paint);
                }

                // 5. 保存
                using (var image = surface.Snapshot())
                using (var data = image.Encode(SKEncodedImageFormat.Jpeg, 90))
                using (var stream = File.OpenWrite(outputPath))
                {
                    data.SaveTo(stream);
                }
            }
        }
        Console.WriteLine("水印添加完成。");
    }
}
相关推荐
Jeff-Tsai1 小时前
Day 12:定义与数据缓存的跨节点失效
.net·架构设计·erp·definition-driven
寂寞的船2 小时前
【Unity Bug挑战】第10期 SerializeReference 内存泄漏之谜
unity·c#·游戏引擎·bug
CSharp精选营2 小时前
从控制台到「会动手」的 AI:用 Semantic Kernel 搭应用(小白教程)
c#·.net·ai agent·semantickernel·functioncalling
Jeff-Tsai1 天前
Day 11:数据访问层的连接、Transaction 与查询条件
.net·架构设计·erp·definition-driven
宝桥南山1 天前
.NET 11 - 尝试一下Blazor中的新特性SupplyParameterFromTempData
microsoft·微软·c#·asp.net·.net·.netcore
1-1=01 天前
C# 中 Lambda、Linq 与 Expression
c#·linq
zhanghaha13141 天前
C# 系列教程_7 判断语句(分支)保姆级精讲
开发语言·c#
zhanghaha13141 天前
C# 系列教程_8 循环语句 保姆级精讲(面向新手)
开发语言·c#