C# 二十年语法变迁之 C# 9参考
自从 C# 于 2000 年推出以来,该语言的规模已经大大增加,我不确定任何人是否有可能在任何时候都对每一种语言特性都有深入的了解。因此,我想写一系列快速参考文章,总结自 C# 2.0 以来所有主要的新语言特性。我不会详细介绍它们中的任何一个,但我希望这个系列可以作为我自己(希望你也是!)的参考,我可以不时回过头来记住我使用的工具工具箱里有。:)
开始之前的一个小提示:我将跳过一些更基本的东西(例如 C# 2.0 引入了泛型,但它们的使用范围如此广泛,以至于它们不值得包括在内);而且我还可以将一些功能“粘合”在一起,以使其更简洁。本系列并不打算成为该语言的权威或历史记录。相反,它更像是可能派上用场的重要语言功能的“备忘单”。您可能会发现浏览左侧的目录以搜索您不认识或需要快速提醒的任何功能很有用。
C# 9.0
仅初始化设置器
这种新语法允许创建可以使用对象初始化语法[1]设置的属性,但再也不会:
public class User {
public string Name { get; init; }
public int Age { get; init; }
}
// ...
var user = new User { Name = "Ben", Age = 30 };
user.Name = "Seb"; // Won't compile, 'Name' is init-only
全屏查看代码[2]• “Init-Only Setters” 仅初始化属性也可以应用访问修饰符,就像常规设置器一样:
public class User {
public string Name { get; init; }
public int Age { get; internal init; } // Age can only be set from within this assembly
}
全屏查看代码[3]• “Internal Init-Only Setter”
Top-Level Statements(顶级语句)
这个小功能可以让您在程序的入口点(即Main()函数)中省略“样板”。这是一个之前和之后的演示:
// Before
using System;
using System.Threading.Tasks;
namespace TestNamespace {
class Program {
static async Task<int> Main(string[] args) {
if (args.Length > 0 && args[0] == "Do It") {
var success = await Database.DownloadData();
if (success) return 0;
else return 1;
}
Console.WriteLine("What should I do? Exiting...");
return 100;
}
}
}
全屏查看代码[4]• “顶级语句-之前”
// After
using System;
using System.Threading.Tasks;
using TestNamespace;
if (args.Length > 0 && args[0] == "Do It") {
var success = await Database.DownloadData();
if (success) return 0;
else return 1;
}
Console.WriteLine("What should I do? Exiting...");
return 100;
全屏查看代码[5]• “顶级语句-之后” 请注意,不再有Main()声明或命名空间声明;编译器为我们合成了它(我们甚至仍然可以使用args数组)。唯一需要注意的是,因为我们不再在TestNamespace命名空间中,我们必须通过using TestNamespace 导入它;如果我们想使用TestNamespace.Database类。
Native-Sized Integers(本机大小的整数)
这个以性能为导向的功能增加了两个新的关键字/别名;nint和nint。这些用于表示本机平台字长的有符号/无符号整数(即 32 位平台上的 32 位,64 位平台上的 64 位等)。
从技术上讲, nint是IntPtr的别名,而nuint是UIntPtr的别名。但是,当使用类型为原生大小整数的变量时,编译器会提供一些额外的算术运算:
nint nativeIntegerOne = 100;
nint nativeIntegerTwo = 200;
IntPtr intPtrOne = new IntPtr(100);
IntPtr intPtrTwo = new IntPtr(200);
Console.WriteLine(nativeIntegerOne + nativeIntegerTwo); // 300
Console.WriteLine(intPtrOne + intPtrTwo); // Doesn't compile
全屏查看代码[6]• “Native Integers vs IntPtrs”
Record Types记录类型
此功能使定义主要用于封装数据的类型变得更加容易(与抽象/封装行为的类型相反)。记录类型是常规类,但编译器会自动在该类型上生成一些成员,以便更轻松地将它们用作数据容器。
public record User(string Name, int Age) { }
全屏查看代码[7]• “简单记录类型定义” 公共记录行User(string Name, int Age) { }声明了一个新的User类类型:
有两个属性:public string Name { get; 在里面; }和公共 int 年龄 { 获取;在里面; }。
有一个构造函数public User(string Name, int Age)将Name分配给this.Name并将Age分配给this.Age(是的,ctor 参数在 PascalCase 中)。
实现IEquatable;如果other是特定的User(而不是派生类型),并且Name和Age的值相等,则Equals(User other)的实现返回 true 。换句话说,记录类型实现了值相等。
重写ToString()以提供报告所有成员值的实现。
提供一个Deconstruct()实现,该实现具有与记录定义中定义的顺序相同的位置参数(即字符串名称,int Age)。
void Test() {
// Constructor
var user = new User("Ben", 30);
// Properties
Console.WriteLine(user.Name); // Ben
Console.WriteLine(user.Age); // 30
// Equality
var user2 = new User("Ben", 30);
var user3 = new User("Seb", 27);
Console.WriteLine(user == user2); // True
Console.WriteLine(user == user3); // False
// ToString
Console.WriteLine(user); // User { Name = Ben, Age = 30 }
Console.WriteLine(user3); // User { Name = Seb, Age = 27 }
// Deconstructor
var (userName, userAge) = user;
Console.WriteLine(userName); // Ben
Console.WriteLine(userAge); // 30
}
全屏查看代码[8]• “生成的成员示例” 默认情况下,记录类型定义不可变(仅限初始化)属性。您可以使用with语句创建具有修改值的记录实例的副本。with语句返回相同记录类型但具有指定修改属性的新
实例
。所有未指定的属性保持不变:
var user = new User("Ben", 30);
user = user with { Age = 31 };
Console.WriteLine(user); // User { Name = Ben, Age = 31 }
全屏查看代码[9]• “使用语句示例记录”
现代软件工程通常认为让您的数据类型不可变可以带来多种好处。将数据复制到具有所需修改的新对象实例中(而不是直接修改现有实例)带来了许多好处,包括使并发更容易理解和不易出错,以及更容易编写类本身(如果没有什么可以改变的话,您只需要在构造函数中验证一次输入;并且您无需担心诸如实现GetHashCode()之类的可变性)。可以在此处找到更多信息:NDepend 博客:C# 不可变类型:了解吸引力[10]。
记录类型名称旁边的位置参数是可选的。我们可以通过以更传统的方式声明属性来创建类似的记录类型:
public record User {
public string Name { get; init; }
public int Age { get; init; }
}
全屏查看代码[11]• “没有位置属性的记录” 这声明了一个与以前具有相同属性的用户记录。由于这是一个记录声明而不是一个类,编译器仍然会为我们生成一个ToString()方法和一个IEquatable实现;并且仍然支持with语句。但是,如果没有位置属性,编译器将不会为我们创建构造函数或解构函数。
我们还可以结合这两种方法来覆盖属性的默认实现。这是一个我们使自动生成的Name属性可变的示例:
public record User(string Name, int Age) {
public string Name { get; set; } = Name;
}
全屏查看代码[12]• “覆盖自动生成的属性” 语法 ' Name { get; 放; } = 名称;' 这里可能看起来有点令人惊讶。事实上,这是一种仅支持记录类型的特殊语法,它告诉编译器我们要将Name构造函数参数分配给Name属性。
这可以与任何属性一起使用:
public record User(string Name, int Age) {
public string Note { get; set; } = $"{Name}, aged {Age}";
}
void Test() {
var user = new User("Ben", 30);
Console.WriteLine(user.Name); // Ben
Console.WriteLine(user.Age); // 30
Console.WriteLine(user.Note); // Ben, aged 30
}
全屏查看代码[13]• “分配构造函数参数” 在为记录类型创建自己的构造函数时,您必须调用编译器生成的构造函数(通过this()调用):
public record User(string Name, int Age) {
public string Note { get; set; } = $"{Name}, aged {Age}";
public User(string name, int age, string note) : this(name, age) { // Without the 'this(name, age)', this ctor will not compile
Note = note;
}
}
void Test() {
Console.WriteLine(new User("Ben", 30).Note); // Ben, aged 30
Console.WriteLine(new User("Ben", 30, "Custom user note").Note); // Custom user note
}
全屏查看代码[14]• “其他记录构造函数” 必须调用编译器生成的构造函数的原因现在应该很明显了。就行了public string Note { get; 放; } = $"{Name},年龄 {Age}"; 我们使用构造函数参数Name和Age为Note分配一个默认值。如果从不调用编译器生成的构造函数,则这些参数将不可用,并且不清楚Note的默认值应该是什么。
增强模式匹配
关系匹配允许使用>、>=、<和<=匹配值范围。类型匹配允许在纯粹匹配对象类型时省略丢弃。下面的示例在switch 表达式中使用属性模式,但关系匹配也可以与大多数其他模式一起使用:
var salary = user switch {
Manager { YearsAtCompany: >= 5, DirectReports: { Count: >= 10 } } => 120_000, // Managers who have worked at the company for at least 5 years and have at least 10 direct reports get 120,000
Manager { YearsAtCompany: >= 5 } => 100_000, // Managers who have worked at the company for at least 5 years get 100,000
Manager => 70_000, // All other managers get 70,000 (notice no discard '_' variable required any more)
{ YearsAtCompany: >= 3, Age: >= 18 } => 50_000, // Anyone else who's at least 18 and has worked
_ => 30_000 // Everyone else gets 30,000
};
全屏查看代码[15]• “关系和类型模式匹配” Conjunctive、disjunctive和否定模式允许您以熟悉的方式组合模式:
/*
* The following code determines whether a player is eligible for an award.
* If the player has a score >= 100, is not dead, and is NOT a MonsterPlayer, return true.
* If the player is a hero who has slain >= 3 monsters, or is a monster who has chomped >= 5 or has >= 200 score, return true.
* Else return false.
*/
var playerIsEligibleForMvpAward = player switch {
Player { Score: >= 100, IsDead: false } and not MonsterPlayer => true,
HeroPlayer { MonstersSlain: >= 3 } or MonsterPlayer { HeroesChomped: >= 5 } or MonsterPlayer { Score: >= 200 } => true,
_ => false
};
全屏查看代码[16]• “连接/分离/否定模式” 在检查变量是否不是给定类型时 ,否定模式特别有用:
// Revive the player if they're not a monster
if (player is not MonsterPlayer) player.Revive();
// Send the player to hell if they're not a hero, otherwise send them to heaven
if (player is not HeroPlayer hero) player.SendToHell();
else hero.SendToHeaven();
全屏查看代码[17]• “否定类型检查”
Target-Typed Expressions目标类型表达式
如果可以推断类型,则目标类型的新表达式允许您在调用构造函数时省略类型名称:
// Field
Dictionary<string, List<(int Age, string Name)>> _userLookupDict = new(); // No need to re-iterate the type "Dictionary<string, List<(int Age, string Name)>>"!
void Test() {
// Locals
List<string> names = new(_userLookupDict.Keys); // Can still pass constructor parameters as usual
User u = new() { Name = "Ben", Age = 31 }; // Can use object initialization syntax as usual
}
全屏查看代码[18]• “目标类型的新表达式”
由于显而易见的原因,目标类型的新表达式与隐式类型的局部变量(即var )不兼容。目标类型条件允许编译器更好地找到条件表达式的两个操作数之间的公共类型。这是一个使用三元条件运算符的示例:
// Assume 'selectManager' is a bool, 'manager' is a Manager (where Manager : User) and 'developer' is a Developer (where Developer : User)
User u = selectManager ? manager : developer;
全屏查看代码[19]• “目标类型三元条件” 在 C# 9 之前,该行无法编译,因为manager和developer是不同类型的变量。但是,现在我们可以将u声明为User类型的变量(它是Manager和User的共享父/基类),并编译该行。
不幸的是,此功能也与隐式类型的本地不兼容。
Covariant Return Types协变返回类型
此功能允许您在覆盖基类方法时指定更衍生的返回类型:
abstract class Player {
public abstract IWeapon GetEquippedWeapon();
}
class MonsterPlayer : Player {
// Here we can specify that the weapon will always be a ClawsWeapon for a MonsterPlayer:
public override ClawsWeapon GetEquippedWeapon() {
// ...
}
}
全屏查看代码[20]• “协变返回类型覆盖”
GetEnumerator 扩展
该功能允许您通过扩展方法将foreach支持添加到任何类型:
// Add a GetEnumerator to UInt16 that iterates through every bit (from MSB to LSB)
public static class UInt16Extensions {
public static IEnumerator<bool> GetEnumerator(this UInt16 @this) {
for (var i = (sizeof(UInt16) * 8) - 1; i >= 0; --i) {
yield return (@this & (1 << i)) != 0;
}
}
}
// Usage example:
// This program writes "1100001100001111" to the console
ushort u = (ushort) 0b1100_0011_0000_1111U;
foreach (var bit in u) {
Console.Write(bit ? 1 : 0);
}
全屏查看代码[21]• “扩展 GetEnumerator() 示例”
模块初始化器
此功能允许我们在模块中的任何其他代码(大多数情况下为 DLL/EXE)之前执行代码。
在编写带有入口点的程序时,此功能可能看起来没那么有用;但是,当为其他人编写库以将其作为依赖项包含在他们自己的应用程序中时,此功能可能会非常方便。假设我们需要在访问我们库中的任何类型之前解析一些定制的依赖关系图。模块初始化器将让我们确保在使用我们的库之前解析图:
static class TypeDependencyResolver {
[ModuleInitializer]
public static void ResolveGraph() {
// Do stuff here
}
}
全屏查看代码[22]• “模块初始化程序示例” 在此示例中,在模块中第一次使用任何类型/方法之前的任何时间,运行时都会自动调用ResolveGraph() 。在初始化函数中可以做的事情没有限制(包括调用其他函数、创建对象、使用 I/O 等);但根据经验,初始化程序不应花费太长时间来执行或有可能引发异常。
模块初始化函数必须是public和static,并返回void。但是它们可以是异步的(即async void)。可以有多个ModuleInitializers 在同一个模块中。多个方法的执行顺序是任意的,取决于运行时。
SkipLocalsInit
这种面向性能的属性可以应用于方法、类型和模块。它指示编译器不要发出运行时标志,该标志通常会告诉运行时在函数开始之前将所有本地声明的变量归零。
通常编译器会在所有方法上发出这个标志,因为它可以保护我们免受某些类型的错误。但是,在某些情况下,将内存归零可能会显着降低性能[23]。当我们应用[SkipLocalsInit]时,我们可以要求编译器跳过这一步:
// Following code will only write "Found a non-zero byte" when [SkipLocalsInit] is applied
[SkipLocalsInit]
public static void Main() {
Span<byte> stackData = stackalloc byte[4000];
for (var i = 0; i < stackData.Length; ++i) {
if (stackData[i] != 0) {
Console.WriteLine("Found a non-zero byte!");
break;
}
}
}
全屏查看代码[24]• “SkipLocalsInit 示例”
函数指针、SuppressGCTransition 和 UnmanagedCallersOnly
托管函数指针
函数指针和[SuppressGCTransition]属性是面向性能的特性,允许简化间接方法调用;托管或非托管/本机。
函数指针是使用delegate语法声明的(并且必须在不安全的上下文中使用)。第一个示例展示了如何使用指向托管函数的指针:
public class User {
public string Name { get; set; }
public int Age { get; set; }
public void ClearUserDetails() {
Name = "<cleared>";
Age = 0;
}
}
public static class Database {
public static int ClearAllRecords(string idPrefix) => idPrefix.Length;
public static void ClearUserDetails(User u) => u.ClearUserDetails();
}
// ...
unsafe {
delegate* managed<string, int> databaseClearFuncPtr = &Database.ClearAllRecords;
Console.WriteLine(databaseClearFuncPtr("Testing")); // Prints '7' on console
var user = new User { Name = "Ben", Age = 31 };
delegate* managed<User, void> userClearFuncPtr = &Database.ClearUserDetails;
userClearFuncPtr(user);
Console.WriteLine($"User: {user.Name} / {user.Age}"); // Prints 'User: <cleared> / 0' on console
}
全屏查看代码[25]• “托管函数指针” 第一个指针 ( databaseClearFuncPtr ) 指向Database.ClearAllRecords。它被声明为一个托管函数指针,它接受一个字符串输入并返回一个int。在下一行调用它类似于调用Func<string, int>。
第二个指针(userClearFuncPtr)显示了如何通过解决对象实例的单一调度来调用非静态函数。[26]我们不能创建指向实例方法的指针(即User.ClearUserDetails()),但我们可以创建一个获取实例并为我们调用相关方法的静态方法。因此,userClearFuncPtr指向Database.ClearUserDetails()。它被声明为一个托管函数指针,它接受用户输入并且不返回任何内容(void)。在下一行调用它类似于调用Action。
非托管函数指针
非托管指针允许您直接存储指向非托管函数的指针。您可能会通过 P/Invoke 调用或其他方式收到此指针。
想象一下,我们有一个具有以下实现的 C++ 库:
static const wchar_t* GetHelloString() {
return L"Hello";
}
typedef const wchar_t* (*helloStrPtr)(void);
extern "C" __declspec(dllexport) void GetFuncPtr(helloStrPtr* outFuncPtr) {
*outFuncPtr = &GetHelloString;
}
全屏查看代码[27]• “示例本机方法声明” GetFuncPtr() 的实现需要一个指向指针的指针,以便它可以将我们的函数指针设置为指向GetHelloString()。
GetFuncPtr()理论上可以只返回一个函数指针,但我创建了这个示例来展示编组指针到指针的可能更困难的用例。在 C# 方面,我们将像这样表示导出的GetFuncPtr():
public static class NativeMethods {
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
public static unsafe extern void GetFuncPtr(delegate* unmanaged<char*>* outFuncPtr);
}
全屏查看代码[28]• “用 C# 表示 GetFuncPtr()” 然后我们可以调用GetFuncPtr()并像这样使用函数指针:
unsafe {
delegate* unmanaged<char*> getHelloStrFuncPtr;
NativeMethods.GetFuncPtr(&getHelloStrFuncPtr);
Console.WriteLine(new String(getHelloStrFuncPtr())); // Writes "Hello" to the console
}
全屏查看代码[29]• “GetFuncPtr() 的使用” 最后,在声明非托管指针时,可以指定调用约定:
delegate* unmanaged<int, int> automaticConventionFuncPtr;
delegate* unmanaged[Cdecl]<int, int> cdeclConventionFuncPtr;
delegate* unmanaged[Fastcall]<int, int> fastcallConventionFuncPtr;
delegate* unmanaged[Stdcall]<int, int> stdcallConventionFuncPtr;
delegate* unmanaged[Thiscall]<int, int> thiscallConventionFuncPtr;
全屏查看代码[30]• “声明非托管函数指针”
抑制GCTransition
请注意,GetFuncPtr()的 C++ 实现非常简单。通常,当通过 P/Invoke 调用本机方法时,运行时将首先设置 GC 以处理向非托管代码的转换。但是,在某些情况下,这种转换可能会增加不必要的开销。当方法被调用时,这是真的:微不足道、完成非常快、不做任何 I/O、不使用任何同步/线程、不抛出异常, 当我们知道方法满足上面列表中的所有条件时,可以 将[SuppressGCTransition]属性应用于外部方法,以告诉运行时不要感染此转换:
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl), SuppressGCTransition]
public static unsafe extern void GetFuncPtr(delegate* unmanaged<char*>* outFuncPtr);
全屏查看代码[31]•“应用了 SuppressGCTransition 的 GetFuncPtr()”
UnmanagedCallersOnly
我们现在可以编写只能通过本机代码中的函数指针调用的方法。与SuppressGCTransition类似,将[UnmanagedCallersOnly]属性应用于方法有助于运行时/编译器减少本地到托管调用的开销。
假设我们有一个 C++ 实现,如下所示:
typedef int (*getIntPtr)(void);
extern "C" __declspec(dllexport) void InvokeFuncPtr(getIntPtr funcPtr) {
std::wcout << funcPtr();
}
全屏查看代码[32]• “UnmanagedCallersOnly 示例,C++ 端” 我们可以使用以下 C# 签名方法来表示此方法:
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
public static unsafe extern void InvokeFuncPtr(delegate* unmanaged[Cdecl]<int> funcPtr);
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
public static int ReturnInt() => 123;
全屏查看代码[33]• “UnmanagedCallersOnly 示例,C# 端” 尝试直接从 C# 调用ReturnInt()将发出编译器错误。相反,我们可以将指向它的指针传递给我们的 C++ 方法:
unsafe {
NativeMethods.InvokeFuncPtr(&NativeMethods.ReturnInt); // Prints 123 to std::wcout (i.e. console)
}
全屏查看代码[34]• “使用 UnmanagedCallersOnly 指针”
注意:将SuppressGCTransition添加到InvokeFuncPtr()声明会导致此程序在运行时崩溃并显示消息
“致命错误。无效程序:试图从托管代码调用 UnmanagedCallersOnly 方法。”
. 这是因为 GC 转换实际上是允许运行时检测是否已从本机调用者调用的方法。
SourceGenerator源生成器
此功能允许您编写将在编译时生成更多代码的代码。此功能只能添加/覆盖代码,不能修改现有代码。
设置
首先,您必须创建一个新的 .NET Standard 类库项目,并通过 NuGet将Microsoft.CodeAnalysis.Analyzers和Microsoft.CodeAnalysis.CSharp添加到您的项目中。这将是源生成器项目,它将在目标项目中生成代码。
源生成器项目必须完全以.NET Standard 2.0为目标(在我的测试中,甚至 2.1 都没有工作;.NET 5 也没有)。这似乎不太可能在不久的将来改变[35]。这个新项目将包含在目标项目中生成代码的代码。为此,我们必须从目标项目中添加对生成器项目的特殊引用。打开目标项目的.csproj文件,添加生成器引用:
<!-- This ItemGroup should be added inside the Project node -->
<ItemGroup>
<ProjectReference Include="..\SourceGen\SourceGen.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
全屏查看代码[36]• “目标项目 CSPROJ 文件” 现在,当我们构建目标项目时,SourceGen项目将在编译期间被编译并执行。SourceGen项目将有机会在编译之前将代码插入到我们的目标项目中。
实现 ISourceGenerator
向实现ISourceGenerator的生成器项目添加一个类。您需要导入Mircosoft.CodeAnalysis命名空间。用[Generator]注释这个类:
[Generator]
public class MySourceGenerator : ISourceGenerator {
public void Execute(GeneratorExecutionContext context) {
// TODO
}
public void Initialize(GeneratorInitializationContext context) {
// TODO
}
}
全屏查看代码[37]• “生成器类存根(在生成器项目中)” Initialize函数可用于注册将创建ISyntaxReceiver[38] 的函数;当编译器在源项目中移动时,它将依次为源项目中的每个语法节点调用其OnVisitSyntaxNode函数。
您还可以通过context.SyntaxReceiver从Execute方法访问实例化的ISyntaxReceiver。下面的示例展示了如何连接一个简单的ISyntaxReceiver,它将所有节点打印到一个文本文件中:
class SyntaxPrinter : ISyntaxReceiver {
readonly FileStream _fs;
readonly TextWriter _tw;
public SyntaxPrinter() {
_fs = File.OpenWrite(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "test.txt"));
_tw = new StreamWriter(_fs);
}
public void OnVisitSyntaxNode(SyntaxNode syntaxNode) {
_tw.WriteLine($"Node received: {syntaxNode.Kind()} {syntaxNode}");
_fs.Flush();
}
}
[Generator]
public class MySourceGenerator : ISourceGenerator {
public void Execute(GeneratorExecutionContext context) {
// TODO
}
public void Initialize(GeneratorInitializationContext context) {
context.RegisterForSyntaxNotifications(() => new SyntaxPrinter());
}
}
全屏查看代码[39]• “生成器类语法接收器示例” 要添加源代码,请执行 Execute函数。源代码生成的潜力可能会填满一篇全新的博客文章,因此在这种情况下,我将仅展示一个将新文件添加到编译中的示例:
[Generator]
public class MySourceGenerator : ISourceGenerator {
const string ExampleSource = @"
namespace GeneratedNamespace {
public static class GeneratedClass {
public static void SayHello() => System.Console.WriteLine(""Hello"");
}
}";
public void Execute(GeneratorExecutionContext context) {
context.AddSource("Generated.cs", ExampleSource);
}
public void Initialize(GeneratorInitializationContext context) {
/* do nothing */
}
}
全屏查看代码[40]• “生成器源添加示例”
请注意,此文件是在编译期间“虚拟”添加的;没有将名为Generated.cs的实际文件添加到目标项目中。这个附加文件在命名空间GeneratedNamespace中的静态类GeneratedClass中声明了一个静态方法SayHello()。在我们的目标项目中,我们可以直接调用这个方法:
using System;
GeneratedNamespace.GeneratedClass.SayHello();
全屏查看代码[41]• “生成器目标源” Intellisense 会抱怨GeneratedNamespace.GeneratedClass.SayHello()不存在,但无论如何我们都可以继续编译它,因为我们知道在这种情况下 Intellisense 不存在某些东西。运行目标项目将在控制台上打印“Hello”。
删除了对部分方法的限制
此新功能还包括对部分方法的一些更改。它消除了部分方法是私有的和返回void的必要性;只要定义由编译时提供。这允许我们提前声明由目标应用程序调用的方法(从而消除智能感知和可发现性问题),但定义由生成器在编译时提供。
我们可以声明一个我们想要通过生成器实现的方法:
using System;
Console.WriteLine(GeneratorTarget.GeneratedClass.GetInt());
namespace GeneratorTarget {
public static partial class GeneratedClass {
public static partial int GetInt();
}
}
全屏查看代码[42]• “带有部分方法的生成器目标源” 不幸的是,我们仍然得到一个智能感知错误,告诉我们我们没有在任何地方提供GetInt()的实现;但至少该错误仅位于GetInt()上,并且仍然允许我们发现将要实现的方法。
此方法的实现如您所料:
[Generator]
public class MySourceGenerator : ISourceGenerator {
const string ExampleSource = @"
namespace GeneratorTarget {
public static partial class GeneratedClass {
public static partial int GetInt() => 123;
}
}";
public void Execute(GeneratorExecutionContext context) {
context.AddSource("Generated.cs", ExampleSource);
}
public void Initialize(GeneratorInitializationContext context) {
/* do nothing */
}
}
全屏查看代码[43]• “使用部分方法的生成器源项目” 运行我们的目标项目会在屏幕上 打印123 。
References
[1]
对象初始化语法: https://benbowen.blog/post/two_decades_of_csharp_i/#object_initializers[2]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/init-only_setters.html[3]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/internal_init-only_setter.html[4]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/top-level_statements;_before.html[5]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/top-level_statements;_after.html[6]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/native_integers_vs_intptrs.html[7]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/simple_record_type_definition.html[8]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/generated_members_example.html[9]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/record_with_statement_example.html[10]
NDepend 博客:C# 不可变类型:了解吸引力: https://blog.ndepend.com/c-sharp-immutable-types-understanding-attraction/[11]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/record_without_positional_properties.html[12]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/overriding_auto_generated_properties.html[13]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/assigning_constructor_parameters.html[14]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/additional_record_constructors.html[15]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/relational_and_type_pattern_matching.html[16]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/conjunctive-disjunctive-negative_patterns.html[17]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/negative_type_check.html[18]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/target-typed_new_expressions.html[19]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/target-typed_ternary_conditional.html[20]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/covariant_return_type_override.html[21]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/extension_getenumerator()_example.html[22]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/module_initializer_example.html[23]
在某些情况下,将内存归零可能会显着降低性能: https://benbowen.blog/post/clearly_too_slow/[24]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/skiplocalsinit_example.html[25]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/managed_function_pointers.html[26]
单一调度来调用非静态函数。: https://en.wikipedia.org/wiki/Dynamic_dispatch#Single_and_multiple_dispatch[27]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/example_native_method_declaration.html[28]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/representing_getfuncptr()_in_csharp.html[29]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/usage_of_getfuncptr().html[30]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/declaring_unmanaged_function_pointers.html[31]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/getfuncptr()_with_suppressgctransition_applied.html[32]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/unmanagedcallersonly_example,_c++_side.html[33]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/unmanagedcallersonly_example,_csharp_side.html[34]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/using_unmanagedcallersonly_pointer.html[35]
似乎不太可能在不久的将来改变: https://github.com/dotnet/roslyn/issues/49249#issuecomment-782516845[36]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/target_project_csproj_file.html[37]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/generator_class_stub_(in_generator_project).html[38]
ISyntaxReceiver: https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.isyntaxreceiver?view=roslyn-dotnet[39]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/generator_class_syntax_receiver_example.html[40]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/generator_source_addition_example.html[41]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/generator_target_source.html[42]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/generator_target_source_with_partial_method.html[43]
全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_v/generator_source_project_with_partial_method.html