June 14th, 2024

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

Amy Peng
Partner Tech Advisor

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

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

  • 使用任意类型别名重构 C# 代码(本篇文章 
  • 使用默认 lambda 参数重构C#代码

所有这些功能都将继续我们的旅程,使我们的代码更具可读性和可维护性,这些被认为是开发人员应该知道的“日常 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 文件 

若要运行该应用程序,请从根目录执行以下 .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 参数。若要继续了解有关此功能的更多信息,请查看以下资源 

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

Author

Amy Peng
Partner Tech Advisor

Amy Peng is a Partner Tech Advisor on the Dev Community Team focused on .NET. Her responsibility is providing technical support on the forum and promote .NET to all the community members.

3 comments

  • 承昊 周

    据我所知,razor页几乎没人用,而很多很多朋友在用aspx页。
    很奇怪,为什么netcore要放弃很多人用的aspx转而去做个没人用的东西呢?
    razor就是个html,要实现功能需要ajax、vue、controller很多配合,要用vscode再去开发。
    对于10人左右的我们这种小团队,aspx的【onclick=后台代码】太方便了,而razor无形中增大了我们几倍的工作量。
    站在小团队、实用性的角度上,希望netcore支持aspx的方式,或者razor支持直接绑定后台代码。

    As far as I know, the razor page is almost unused, while many friends are using the aspx page.
    It's strange why Netcore has to give up the aspx that many people use and instead make something that no one uses?
    Razor is just HTML, and to achieve its functionality, it requires a lot of collaboration from Ajax, Vue, and controller. It needs to be developed using VScode.
    For our small...

    Read more
    • 万雅虎

      你能搞清楚 Razor Pages 和 WebForms的区别 再来吐槽吗?! 两者是完全不同的开发模式 , 另外后者的性能是相当堪忧的,已经不符合现代网页开发的大流了! 至于语法上aspx的语法 相对于razor语法也是冗长低效的,不能因为你们习惯了原来的编码就忽略了 现代编码的好处. 建议你们团队可以适当学习下新的框架和知识

  • 承昊 周

    please Support ASPX page on .netcore.
    how many people are using razor-page?it is a html.
    i know very more persons are using ASPX-page. i don’t known why netcore not support aspx?
    design for using,not for designing.
    设计是为了用的,不是为了设计而设计的,更不是为了显得自己高明而设计的。