IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    C#12新功能合集三:使用任意类型别名重构C#代码

    Amy Peng发表于 2024-06-14 08:44:56
    love 0

    本文翻译于David Pine的这篇文章: Refactor your code using alias any type。

    这篇文章是四篇系列文章中的第三篇,主要探讨C# 12的各种功能。在这篇文章中,我们将深入探讨“别名任何类型”功能,该功能允许您使用 using 指令为任何类型创建别名。这个系列已经初具规模: 

    • 使用主构造函数重构 C# 代码 
    • 使用集合表达式重构 C# 代码 
    • 使用任意类型别名重构 C# 代码(本篇文章) 
    • 重构 C# 代码以使用默认 lambda 参数 

    所有这些功能都将继续我们的旅程,使我们的代码更具可读性和可维护性,这些被认为是开发人员应该知道的“日常 C#”功能。让我们深入了解吧! 

    别名任意类型*⃣ 

    C# 12 引入了使用 using 指令为任意类型添加别名的功能。此功能允许您指定映射到其他类型的别名。这包括元组类型、指针类型、数组类型,甚至非开放泛型类型,所有这些类型都可以在您的代码中使用。此功能在以下场景特别有用: 

    • 当使用长或复杂的类型名称时。 
    • 当消除类型歧义并解决潜在的命名冲突时。 
    • 当定义要在程序集中共享的值元组类型时。 
    • 当希望通过使用更具描述性的名称来增加代码的清晰度时。 

    官方 C# 文档提供了很多有关如何使用此功能的示例,但我并不想在此重复这些示例,而是决定编写一个演示应用程序来示范该功能的各个方面。

    可空引用类型 

    此功能支持大多数类型,但可空引用类型除外。也就是说,您无法为可空引用类型设置别名, C# 编译器会报告错误 CS9132:使用的别名不能是可空引用类型。以下内容摘自功能说明,以帮助澄清这一点: 

    // This is not legal.
    // Error CS9132: Using alias cannot be a nullable reference type
    using X = string?;
    
    // This is legal.
    // The alias is to `List<...>` which is itself not a nullable
    // reference type itself, even though it contains one as a type argument.
    using Y = System.Collections.Generic.List<string?>;
    
    // This is legal.
    // This is a nullable *value* type, not a nullable *reference* type.
    using Z = int?;

    示例应用程序:UFO 目击事件 

    演示应用程序可在 GitHub 上的 IEvangelist/alias-any-type 上获取。这是一个简单的控制台应用程序,可模拟不明飞行物 (UFO) 目击事件。如果您想在本地执行,可以在您选择的工作目录中使用以下任何一种方法: 

    使用 Git CLI: 

    git clone https://github.com/IEvangelist/alias-any-type.git

    使用 GitHub CLI: 

    gh repo clone IEvangelist/alias-any-type

    下载 zip 文件: 

    如果您想要下载源代码,可以通过以下 URL 获取一个 zip 文件: 

    • IEvangelist/alias-any-type source zip 

    若要运行该应用程序,请从根目录执行以下 .NET CLI 命令: 

    dotnet run --project ./src/Alias.AnyType.csproj

    当应用程序启动时,它会在控制台上打印一条介绍,并等待用户输入后再继续。 

    Image app start

    按下任意键(例如 Enter 键)后,应用会随机生成有效坐标(纬度和经度),然后使用该坐标检索与该坐标相关的地理编码元数据。坐标以度–分–秒格式表示(包括基数)。当应用运行时,会计算生成的坐标之间的距离并将其报告为 UFO 目击事件。 

    Image app run

    若要停止应用程序,请按 Ctrl + C 键。 

    虽然这个应用程序很简单,但它确实包含了其他一些与我们本篇文章重点不一定相关的 C# 代码。我一定会尽量少谈外围主题,但当我认为它们很重要时,我会涉及它们。 

    代码演练👀 

    我们将通过本节一起了解代码库。我想重点介绍一下代码中的几个有趣方面,包括项目文件、GlobalUsings.cs、一些扩展和 Program.cs 文件。在可用的代码中,有些内容我们不会介绍,例如响应模型和几个实用方法。 

    └───📂 src
         ├───📂 Extensions
         │    └─── CoordinateExtensions.cs
         ├───📂 ResponseModels
         │    ├─── GeoCode.cs
         │    ├─── Informative.cs
         │    └─── LocalityInfo.cs
         ├─── Alias.AnyType.csproj
         ├─── CoordinateGeoCodePair.cs
         ├─── GlobalUsings.cs
         ├─── Program.cs
         ├─── Program.Http.cs
         └─── Program.Utils.cs

    我们先来看一下项目文件: 

    <Project Sdk=”Microsoft.NET.Sdk”>

    <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup>

    <ItemGroup> <Using Include=”System.Console” Static=”true” />

    <Using Include=”System.Diagnostics” /> <Using Include=”System.Net.Http.Json” /> <Using Alias=”AsyncCancelable” Include=”System.Runtime.CompilerServices.EnumeratorCancellationAttribute” /> <Using Include=”System.Text” /> <Using Include=”System.Text.Json.Serialization” /> <Using Include=”System.Text.Json” /> </ItemGroup>

    </Project>

    这里要注意的第一件事是,ImplicitUsings 属性设置为enable。此功能自 C# 10 以来就已经存在了,它使目标 SDK(在本例中为 Microsoft.NET.Sdk)能够默认隐式包含一组命名空间。不同的 SDK 包含不同的默认命名空间,有关更多信息,请参阅隐式使用指令文档。 

    Implicit Using指令 

    ImplicitUsing 元素是 MS Build 的一项功能,而 global 关键字是 C# 语言的一项功能。既然我们已选择使用global using功能,我们还可以通过添加自己的指令来利用此功能。添加这些指令的方法之一是向 ItemGroup 添加Using 元素。一些 using 指令在 Static 属性设置为 true 的情况下添加,这意味着它们的所有静态成员都可以无条件使用 – 稍后会详细介绍。Alias 属性用于为类型创建别名,在此示例中,我们为 System.Runtime.CompilerServices.EnumeratorCancellationAttribute 类型指定了 AsyncCancelable 别名。在我们的代码中,我们现在可以使用 AsyncCancelable 作为 EnumeratorCancellation 属性的类型别名。其他 Using 元素为其相应的命名空间创建非静态和非别名的global using 指令。 

    一种新兴模式 🧩 

    我们开始在现代 .NET 代码库中看到一种常见的模式,即开发人员定义一个 GlobalUsings.cs 文件来将所有(或大多数)using指令封装到一个文件中。此演示应用程序遵循此模式,接下来让我们看一下该文件: 

    // Ensures that all types within these namespaces are globally available.
    global using Alias.AnyType;
    global using Alias.AnyType.Extensions;
    global using Alias.AnyType.ResponseModels;
    
    // Expose all static members of math.
    global using static System.Math;
    
    // Alias a coordinates object.
    global using Coordinates = (double Latitude, double Longitude);
    
    // Alias representation of degrees-minutes-second (DMS).
    global using DMS = (int Degree, int Minute, double Second);
    
    // Alias representation of various distances in different units of measure.
    global using Distance = (double Meters, double Kilometers, double Miles);
    
    // Alias a stream of coordinates represented as an async enumerable.
    global using CoordinateStream = System.Collections.Generic.IAsyncEnumerable<
        Alias.AnyType.CoordinateGeoCodePair>;
    
    // Alias the CTS, making it simply "Signal".
    global using Signal = System.Threading.CancellationTokenSource;

    此文件中的所有内容都是global using指令,使别名类型、静态成员或命名空间在整个项目中都可用。前三个指令用于公共命名空间,它们在整个应用程序中的多个位置使用。下一个指令是 System.Math 命名空间的global using static 指令,它使 Math 的所有静态成员都可以无条件使用。其余指令是global using指令,它们为各种类型创建别名,包括几个元组、一个坐标流和一个 CancellationTokenSource,现在可以通过 Signal 轻松引用它们。 

    需要考虑的一件事是,当您定义一个元组别名类型时,如果需要添加行为或其他属性,您可以轻松地将其转换为record类型。例如,以后您可能想为 Coordinates 类型添加一些功能,并且可以轻松地将其更改为record类型: 

    namespace Alias.AnyType;
    
    public readonly record struct Coordinates(
        double Latitude, 
        double Longitude);

    当您定义别名时,您实际上并不是在创建类型,而是在创建引用现有类型的名称。对于元组,您正在定义值元组的形状。当您为数组类型添加别名时,您不是在创建新的数组类型,而是在为该类型添加可能更具描述性的名称。例如,当我定义返回 IAsyncEnumerable<CoordinateGeoCodePair> 的 API 时,需要编写大量代码。而现在,我可以在整个代码库中将其返回类型引用为 CoordinateStream。 

    引用别名📚 

    我们定义了几个别名,一些在项目文件中,另一些在 GlobalUsings.cs 文件中。让我们看看这些别名在代码库中是如何使用的。首先查看顶层 Program.cs 文件: 

    using Signal signal = GetCancellationSignal();
    
    WriteIntroduction();
    
    try
    {
        Coordinates? lastObservedCoordinates = null;
    
        await foreach (var coordinate
            in GetCoordinateStreamAsync(signal.Token))
        {
            (Coordinates coordinates, GeoCode geoCode) = coordinate;
    
            // Use extension method, that extends the aliased type.
            var cardinalizedCoordinates = coordinates.ToCardinalizedString();
    
            // Write UFO coordinate details to the console.
            WriteUfoCoordinateDetails(coordinates, cardinalizedCoordinates, geoCode);
    
            // Write travel alert, including distance traveled.
            WriteUfoTravelAlertDetails(coordinates, lastObservedCoordinates);
    
            await Task.Delay(UfoSightingInterval, signal.Token);
    
            lastObservedCoordinates = coordinates;
        }
    }
    catch (Exception ex) when (Debugger.IsAttached)
    {
        // https://x.com/davidpine7/status/1415877304383950848
        _ = ex;
        Debugger.Break();
    }

    上述代码片段显示了如何使用 Signal 别名创建 CancellationTokenSource 实例。您可能知道,CancellationTokenSource 类是 IDisposable 的一个实现,这就是为什么我们可以使用 using 语句来确保 Signal 实例在超出作用域时得到正确处置。您的 IDE 理解这些别名,当您将鼠标悬停在它们上面时,您将看到它们所代表的实际类型。请查看以下屏幕截图: 

    Image alias hover

    在进入 try / catch 之前,会通过WriteIntroduction 调用将简介写入控制台。try 块包含一个 await foreach 循环,该循环遍历一个 IAsyncEnumerable<CoordinateGeoCodePair>方法。GetCoordinateStreamAsync 方法在单独的文件中定义。我发现自己在编写顶级程序时更经常利用部分类功能,因为它有助于隔离问题。所有基于 HTTP 的功能都在 Program.Http.cs 文件中定义,让我们重点介绍 GetCoordinateStreamAsync 方法: 

    static async CoordinateStream GetCoordinateStreamAsync(
        [AsyncCancelable] CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
    
        do
        {
            var coordinates = GetRandomCoordinates();
    
            if (await GetGeocodeAsync(coordinates, token) is not { } geoCode)
            {
                break;
            }
    
            token.ThrowIfCancellationRequested();
    
            yield return new CoordinateGeoCodePair(
                Coordinates: coordinates, 
                GeoCode: geoCode);
        }
        while (!token.IsCancellationRequested);
    }

    您会注意到它返回 CoordinateStream 别名,即 IAsyncEnumerable<CoordinateGeoCodePair>。它接受 AsyncCancelable 属性,该属性是 EnumeratorCancellationAttribute 类型的别名。此属性用于修饰取消令牌,以便与 IAsyncEnumerable 结合使用来取消。在请求未取消时,该方法会生成随机坐标、检索地理编码元数据并生成新的 CoordinateGeoCodePair 实例。GetGeocodeAsync 方法请求给定坐标的地理编码元数据,如果请求成功,则返回 GeoCode 响应模型。例如,Microsoft Campus 具有如下坐标: 

    GET /data/reverse-geocode-client?latitude=47.637&longitude=-122.124 HTTP/1.1
    Host: api.bigdatacloud.net
    Scheme: https

    若要查看 JSON,请在浏览器中打开此链接。CoordinateGeoCodePair 类型没有别名,但它是一个包含 Coordinates 和 GeoCode 的只读记录结构: 

    namespace Alias.AnyType;
    
    internal readonly record struct CoordinateGeoCodePair(
        Coordinates Coordinates,
        GeoCode GeoCode);

    回到 Program 类,当我们遍历每个坐标地理编码对时,我们将元组解构为 Coordinates 和 GeoCode 实例。Coordinates 类型是两个double值元组的别名,分别表示纬度和经度。同样,将鼠标悬停在 IDE 中的此类型上能快速查看类型,请查看以下屏幕截图: 

    Image alias hover tuple

    GeoCode 类型是一个响应式模型,其中包含有关地理编码元数据的信息。然后,我们使用扩展方法将Coordinates转换为基数化字符串,该字符串以度–分–秒格式表示坐标。我个人喜欢在我的代码库中轻松使用别名。让我们看看一些扩展或返回别名类型的扩展方法: 

    internal static string ToCardinalizedString(this Coordinates coordinates)
    {
        var (latCardinalized, lonCardinalized) = (
            FormatCardinal(coordinates.Latitude, true),
            FormatCardinal(coordinates.Longitude, false)
        );
    
        return $"{latCardinalized},{lonCardinalized}";
    
        static string FormatCardinal(double degrees, bool isLat)
        {
            (int degree, int minute, double second) = degrees.ToDMS();
    
            var cardinal = degrees.ToCardinal(isLat);
    
            return $"{degree}°{minute}'{second % 60:F4}\"{cardinal}";
        }
    }

    此扩展方法扩展了 Coordinates 别名类型并返回坐标的字符串表示形式。它使用 ToDMS 扩展方法将纬度和经度转换为度、分、秒格式。ToDMS 扩展方法定义如下: 

    internal static DMS ToDMS(this double coordinate)
    {
        var ts = TimeSpan.FromHours(Abs(coordinate));
    
        int degrees = (int)(Sign(coordinate) * Floor(ts.TotalHours));
        int minutes = ts.Minutes;
        double seconds = ts.TotalSeconds;
    
        return new DMS(degrees, minutes, seconds);
    }

    如果您还记得的话,DMS 别名是 是由代表度、分和秒的三个值组成的元组。ToDMS 扩展方法接收double值并返回 一个DMS 元组。ToCardinal 扩展方法用于确定坐标的基本方向,返回 N、S、E 或 W。Abs、Sign 和 Floor 方法都是 System.Math 命名空间的静态成员,该命名空间在 GlobalUsings.cs 文件中有别名。 

    除此之外,该应用程序还会向控制台显示 UFO 目击详情,包括坐标、地理编码元数据以及目击之间的距离。这个操作会不断重复,直到用户使用Ctrl + C组合键停止应用程序。 

    后续步骤 

    请务必在自己的代码中尝试一下!稍后请查看本系列的最后一篇文章,我们将在其中探讨默认的 lambda 参数。若要继续了解有关此功能的更多信息,请查看以下资源: 

    • C# using指令:using alias 
    • 允许using alias指令引用任何类型 
    • 元组类型(C# 参考) 
    • .NET SDK 项目的 MSBuild 参考:启用 ImplicitUsings 

    如果您有任何技术问题,欢迎来Microsoft Q&A 提问。

    The post C#12新功能合集三:使用任意类型别名重构C#代码 appeared first on .NET中文官方博客.



沪ICP备19023445号-2号
友情链接