编程工具网址导航

  • 常用
  • 必应
  • 百度
  • 淘宝
  • 360
  • B站
  • 微博
  • GitHub

C# 

10个可优化您C#代码技巧

作为一名充满激情的C#开发人员,我一直在寻找提升编码技能的途径。我很高兴与你分享一些令人惊叹的技巧和见解,这些技巧帮助我成为了更好的程序员。

1、使用调用者信息属性进行更好的调试和日志记录
调用者信息属性允许你获取调用方法的调用者信息,这对于调试和日志记录非常有用。



public void Log(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0)
{
    Console.WriteLine($"[{memberName}({lineNumber}) in {filePath}]: {message}");
}



在上面的示例中,'Log()' 方法使用调用者信息属性自动将调用者的成员名称、文件路径和行号包含在日志消息中。这使得更容易追踪日志消息的来源,而无需手动包含这些信息。

2、使用Span<T>创建高效内存集合
Span<T> 是在C# 7.2中引入的一种高效内存集合,它允许你在不创建新数组或引发不必要的堆分配的情况下处理连续的内存块。它可以用于创建现有数组或其他内存段的“切片”,而不需要复制数据。



public static void ProcessLargeArray(byte[] largeArray)
{
    const int chunkSize = 1024;
    for (int i = 0; i < largeArray.Length; i += chunkSize)
    {
        int length = Math.Min(chunkSize, largeArray.Length - i);
        Span<byte> chunk = largeArray.AsSpan(i, length);
        ProcessChunk(chunk);
    }
}

public static void ProcessChunk(Span<byte> chunk)
{
    // Process the chunk of data without creating a new array
    for (int i = 0; i < chunk.Length; i++)
    {
        chunk[i] *= 2;
    }
}



它的工作原理和其有用性:

Span<T> 的工作原理是基于内存的引用和切片。它允许你引用现有的内存块,而无需复制其内容。这对于处理大型数据集或需要高性能的应用程序非常有用。

减少内存分配:Span<T> 允许你避免创建不必要的数组,从而减少了内存分配的开销。这对于内存敏感的应用程序非常重要。
性能提升:由于避免了数据复制,Span<T> 提供了更快的数据访问速度。这对于需要高性能的应用程序非常有帮助。
安全性:Span<T> 提供了安全的内存访问,防止越界访问和内存泄漏。这有助于减少编程错误和提高应用程序的可靠性。
易于使用:Span<T> 的接口直观且易于使用,可以轻松地创建和操作内存切片,而无需繁琐的内存管理代码。
总的来说,Span<T> 是一个强大的工具,可用于处理内存中的数据,提高性能,减少内存分配,并提高代码的可读性。

3、使用 '_' 来忽略不需要的值
'_' 用于在特定上下文中忽略不需要的值,使代码更加简洁和易于理解。



(int min, _) = GetMinMax(numbers);
Console.WriteLine($"Minimum: {min}");



在上面的示例中,'_' 用于忽略 'GetMinMax()' 方法返回的 'max' 值。这明确表明在这个上下文中不需要或不使用最大值。

in out parameters

if (int.TryParse("123", out _))
{
    Console.WriteLine("The input is a valid integer.");
}


当你不需要实际值时,可以在具有 'out' 参数的情况下使用丢弃,这可以使代码更加简洁和易于阅读。

in pattern matching


if (shape is Circle _)
{
    Console.WriteLine("The shape is a circle.");
}



在模式匹配中可以使用“_”,当你只需要测试类型而不需要实际值时,这可以简化代码。

4、高性能的管道流处理
Pipelines 是.NET中的一个高性能流处理库,旨在高效处理具有低延迟要求的大型数据流。System.IO.Pipelines 命名空间提供了以流式方式读取和写入数据的抽象,同时最小化了内存分配和复制。



public static async Task ProcessStreamAsync(Stream stream)
{
    var pipe = new Pipe();
    var writeTask = FillPipeAsync(stream, pipe.Writer);
    var readTask = ReadPipeAsync(pipe.Reader);

    await Task.WhenAll(writeTask, readTask);
}

private static async Task FillPipeAsync(Stream stream, PipeWriter writer)
{
    const int minimumBufferSize = 512;

    while (true)
    {
        var memory = writer.GetMemory(minimumBufferSize);
        int bytesRead = await stream.ReadAsync(memory);

        if (bytesRead == 0)
        {
            break;
        }

        writer.Advance(bytesRead);

        var result = await writer.FlushAsync();

        if (result.IsCompleted)
        {
            break;
        }
    }

    writer.Complete();
}

private static async Task ReadPipeAsync(PipeReader reader)
{
    while (true)
    {
        var result = await reader.ReadAsync();
        var buffer = result.Buffer;

        foreach (var segment in buffer)
        {
            // Process the data in the segment
            Console.WriteLine($"Read {segment.Length} bytes");
        }

        reader.AdvanceTo(buffer.End);

        if (result.IsCompleted)
        {
            break;
        }
    }

    reader.Complete();
}

public static async Task UsePipelinesAsync()
{
    using var stream = new MemoryStream(Encoding.UTF8.GetBytes("Hello, Pipelines!"));
    await ProcessStreamAsync(stream);
}



在上面的示例中,ProcessStreamAsync方法演示了如何使用Pipe类处理来自Stream对象的数据。该方法创建一个新的Pipe实例并启动两个任务:一个用于向管道写入数据(FillPipeAsync),另一个用于从管道读取数据(ReadPipeAsync)。Task.WhenAll方法用于等待两个任务的完成。

FillPipeAsync方法从输入流中读取数据并将其写入PipeWriter。它在循环中执行此操作,从PipeWriter获取内存,将数据从流中读入内存,并将写入器前进。然后调用PipeWriter.FlushAsync方法,以表示数据可供读取,循环将继续,直到流耗尽或管道完成。

ReadPipeAsync方法从PipeReader中读取数据并在循环中进行处理。它等待PipeReader.ReadAsync方法,该方法返回一个包含ReadOnlySequence<byte>缓冲区的ReadResult。缓冲区以段的方式进行处理,并调用PipeReader.AdvanceTo方法以表示数据已被消耗。循环将继续,直到管道完成。

使用Pipelines进行流处理可以提供显著的性能优势,特别是在需要低延迟处理和最小内存分配的情景中。该库的抽象和设计使其更容易处理复杂的流式场景,如网络通信或文件I/O,具有高效的资源管理和最佳性能。

5、条件弱引用表用于元数据关联
条件弱引用表允许你将元数据与对象关联起来,而不修改它们的原始结构。它使用弱引用,因此在对象不再使用时不会阻止垃圾收集器回收这些对象。


public class Person
{
    public string Name { get; set; }
}

public static class PersonMetadata
{
    private static readonly ConditionalWeakTable<Person, Dictionary<string, object>> MetadataTable = new();

    public static void SetMetadata(Person person, string key, object value)
    {
        var metadata = MetadataTable.GetOrCreateValue(person);
        metadata[key] = value;
    }

    public static object GetMetadata(Person person, string key)
    {
        if (MetadataTable.TryGetValue(person, out var metadata) && metadata.TryGetValue(key, out var value))
        {
            return value;
        }

        return null;
    }
}



在上面的示例中,Person类没有内置的元数据支持。PersonMetadata静态类使用ConditionalWeakTable,将元数据与Person实例关联起来,而不修改原始类。这种方法在你希望在不改变对象结构或创建强引用的情况下存储附加信息时非常有用,强引用可能会阻止垃圾收集。

PersonMetadata类中的SetMetadata和GetMetadata方法允许你存储和检索Person对象的元数据。元数据存储在一个字典中,然后使用ConditionalWeakTable将其与对象关联起来。该表持有对对象的弱引用,因此当对象不再使用并且有资格进行垃圾收集时,关联的元数据也将被收集。

6、使用表达式树进行高级反射技术
表达式树是C#中的一个强大功能,它允许你将代码表示为数据结构,这些数据结构可以在运行时进行操作和编译。这对于高级反射场景非常有用,比如生成动态方法或优化性能关键的代码路径。


public static Func<T, object> GeneratePropertyGetter<T>(string propertyName)
{
    var parameter = Expression.Parameter(typeof(T), "obj");
    var property = Expression.Property(parameter, propertyName);
    var conversion = Expression.Convert(property, typeof(object));
    var lambda = Expression.Lambda<Func<T, object>>(conversion, parameter);

    return lambda.Compile();
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public static void UseExpressionTree()
{
    var person = new Person { Name = "John Doe", Age = 30 };

    var nameGetter = GeneratePropertyGetter<Person>("Name");
    var ageGetter = GeneratePropertyGetter<Person>("Age");

    Console.WriteLine($"Name: {nameGetter(person)}, Age: {ageGetter(person)}");
}



在上面的示例中,GeneratePropertyGetter<T> 方法演示了如何使用表达式树为给定的类和属性名生成属性获取器。该方法接受一个类型参数 T 和表示属性名的字符串,然后构建一个表示访问 T 实例上属性并返回其值的表达式树。

表达式树是使用 Expression 类的方法构建的,例如 Expression.Parameter、Expression.Property 和 Expression.Lambda。一旦表达式树完成,就调用Compile方法生成一个可用于在运行时调用属性获取器的 Func<T, object> 委托。

在UseExpressionTree方法中,使用GeneratePropertyGetter方法创建了Person类的Name和Age属性的属性获取器。然后,这些属性获取器用于从Person对象中检索属性值。

使用表达式树进行高级反射技术可以提供多种好处,如提高性能、灵活性以及生成和编译代码的能力。但请注意,表达式树可能比传统的反射技术更复杂且更难调试,因此请谨慎使用,仅在必要时使用。

7、使用通道简化多线程处理
通道是在.NET Core 3.0中引入的一种同步原语,通过提供一种线程安全的方式,简化了多线程处理,使线程能够进行通信和数据交换。它们可以用于实现生产者-消费者模式,允许你解耦数据的生产和消费。


public static async Task ProcessDataAsync()
{
    var channel = Channel.CreateUnbounded<int>();

    var producer = Task.Run(async () =>
    {
        for (int i = 1; i <= 10; i++)
        {
            Console.WriteLine($"Produced: {i}");
            await channel.Writer.WriteAsync(i);
            await Task.Delay(1000);
        }
        channel.Writer.Complete();
    });

    var consumer = Task.Run(async () =>
    {
        await foreach (var item in channel.Reader.ReadAllAsync())
        {
            Console.WriteLine($"Consumed: {item}");
        }
    });

    await Task.WhenAll(producer, consumer);
}



在上面的示例中,ProcessDataAsync方法演示了使用通道进行简单的生产者-消费者场景。通道变量是一个无限容量的通道,这意味着它可以存储无限数量的项。

生产者任务生成数据(从1到10的整数)并使用WriteAsync方法将其写入通道。消费者任务使用ReadAllAsync方法从通道中读取数据并进行处理。在这种情况下,它只是将消耗的数据打印到控制台。

生产者和消费者任务并发运行,允许消费者在数据可用时立即处理数据。Channel类确保数据交换是线程安全的,使得编写多线程代码变得更容易,而无需担心锁定或其他同步机制。

通道可以在各种情景中使用,例如数据处理管道、并行工作负载或在多线程应用程序中实现组件之间的通信。它们提供了一种易于使用且高效的方式来管理线程之间的并发和数据交换。

8、使用Roslyn进行动态代码编译
使用Roslyn进行动态代码编译允许你在运行时编译和执行C#代码。这对于脚本、插件或需要动态生成或修改代码的情况非常有用。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Reflection;

public static async Task ExecuteDynamicCodeAsync(string code)
{
    string sourceCode = $@"
using System;

namespace DynamicCode
{{
    public class Runner
    {{
        public static void Run()
        {{
            {code}
        }}
    }}
}}";

    var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
    var references = new List<MetadataReference>
    {
        MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
    };

    var compilation = CSharpCompilation.Create("DynamicCode")
        .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
        .AddReferences(references)
        .AddSyntaxTrees(syntaxTree);

    using var ms = new MemoryStream();
    var result = compilation.Emit(ms);

    if (!result.Success)
    {
        Console.WriteLine("Compilation failed");
        return;
    }

    ms.Seek(0, SeekOrigin.Begin);
    var assembly = Assembly.Load(ms.ToArray());
    var type = assembly.GetType("DynamicCode.Runner");
    var method = type.GetMethod("Run", BindingFlags.Static | BindingFlags.Public);
    method.Invoke(null, null);
}

await ExecuteDynamicCodeAsync("Console.WriteLine(\"Hello from dynamic code!\");");


在上面的示例中,ExecuteDynamicCodeAsync方法演示了如何使用Roslyn编译和执行运行时的一段C#代码。输入代码参数嵌入到一个简单的类定义中,放在sourceCode变量中。

Roslyn的CSharpSyntaxTree.ParseText方法用于将源代码解析为语法树,然后将其添加到一个新的CSharpCompilation对象中。编译对象还添加了必要的程序集引用,包括对System命名空间的引用。

Emit方法将代码编译成一个动态链接库(DLL)并将输出写入MemoryStream。如果编译成功,生成的程序集将使用Assembly.Load加载到当前应用程序域中。然后,使用反射获取Runner类及其Run方法,并调用该方法执行动态代码。

这种技术允许你构建灵活和可扩展的应用程序,可以在运行时动态编译和执行C#代码。但是,要注意安全性方面的影响,因为执行任意代码如果不正确处理可能会引入安全风险。

9、将匿名类型转换为动态类型以实现灵活的数据操作
将匿名类型转换为动态对象可以在操作数据时提供更大的灵活性。匿名类型是只读的和强类型的,这在你想要修改或扩展数据时可能会有限制。通过将匿名类型转换为动态的ExpandoObject,你可以在运行时添加、删除或修改属性。



public static dynamic ToDynamic(object anonymousObject)
{
    var dynamicObject = new ExpandoObject() as IDictionary<string, object>;
    var properties = TypeDescriptor.GetProperties(anonymousObject);

    foreach (PropertyDescriptor property in properties)
    {
        dynamicObject.Add(property.Name, property.GetValue(anonymousObject));
    }

    return dynamicObject;
}

public static void ManipulateData()
{
    var anonymousData = new { Name = "John", Age = 30 };
    dynamic dynamicData = ToDynamic(anonymousData);

    Console.WriteLine($"Name: {dynamicData.Name}, Age: {dynamicData.Age}");

    dynamicData.Age = 35;
    dynamicData.City = "New York";

    Console.WriteLine($"Name: {dynamicData.Name}, Age: {dynamicData.Age}, City: {dynamicData.City}");
}



在上面的示例中,ToDynamic方法以匿名对象作为输入,将其转换为动态ExpandoObject。它通过使用TypeDescriptor.GetProperties遍历匿名对象的属性,并使用IDictionary<string, object>接口将它们添加到ExpandoObject中来实现这一点。

ManipulateData方法演示了如何使用ToDynamic方法将匿名对象转换为动态对象。anonymousData变量保存一个具有Name和Age属性的匿名对象。在将其转换为动态对象后,你可以直接访问和修改其属性,还可以添加新的属性,如City。

将匿名类型转换为动态对象在需要更灵活的数据结构时可以非常有用,特别是在处理动态生成的数据或在编译时不知道架构的情况下。然而,请记住,使用动态对象可能会失去编译时类型检查,并可能带来性能开销。

10、创建可重复使用资源的简单对象池
对象池是一种设计模式,它有助于重复使用昂贵创建的对象,比如数据库连接或大型内存缓冲区。通过创建一组预分配的对象,并在需要时重复使用它们,可以提高应用程序的性能,并减少与创建和销毁对象相关的开销。



public class ObjectPool<T> where T : new()
{
    private readonly ConcurrentBag<T> _objects;
    private readonly Func<T> _objectGenerator;

    public ObjectPool(Func<T> objectGenerator = null)
    {
        _objectGenerator = objectGenerator ?? (() => new T());
        _objects = new ConcurrentBag<T>();
    }

    public T GetObject()
    {
        if (_objects.TryTake(out T item))
        {
            return item;
        }

        return _objectGenerator();
    }

    public void ReturnObject(T item)
    {
        _objects.Add(item);
    }
}

public class ExpensiveResource
{
    public int Value { get; set; }
}

public static void UseObjectPool()
{
    var pool = new ObjectPool<ExpensiveResource>();

    var resource = pool.GetObject();
    resource.Value = 42;

    Console.WriteLine($"Resource value: {resource.Value}");

    pool.ReturnObject(resource);
}



在上面的示例中,ObjectPool<T>类是对象池的通用实现。它使用ConcurrentBag<T>来存储对象池中的对象,使用Func<T>委托在需要时创建新对象。GetObject方法从对象池中检索一个对象(如果有的话),如果池为空,则创建一个新对象。ReturnObject方法在不再需要对象时将对象返回到池中。

ExpensiveResource类代表了一个昂贵的资源,需要大量时间来创建。在UseObjectPool方法中,创建了一个ObjectPool<ExpensiveResource>的实例,然后使用GetObject方法从池中获取ExpensiveResource对象。在操作对象的属性后,调用ReturnObject方法将对象返回到池中以供将来重用。

使用对象池可以通过最小化对象分配和释放的次数来提高应用程序的性能,并减少内存使用。这在高性能或资源受限的环境中特别有用,其中对象创建和垃圾收集可能是重要的瓶颈。但是,请记住,对象池会增加代码的复杂性,可能不适用于所有情景。