C#7为C#语言添加了许多新功能:
out
变量:- 您可以将
out
内联值声明为使用它们的方法的参数。
- 您可以将
- 元组
- 您可以创建包含多个公共字段的轻量级,未命名的类型。编译器和IDE工具可以理解这些类型的语义。
- 模式匹配
- 您可以根据这些类型的成员的任意类型和值创建分支逻辑。
ref
当地人和回报- 方法参数和局部变量可以是对其他存储的引用。
- 本地功能
- 您可以将功能嵌套在其他功能中,以限制其范围和可见性。
- 更健康的成员
- 可以使用表达式创作的成员列表已经增加。
throw
表达式- 您可以在以前不允许的代码结构中抛出异常,因为它
throw
是一个语句。
- 您可以在以前不允许的代码结构中抛出异常,因为它
- 广义异步返回类型
- 用
async
修饰符声明的方法可以返回除了Task
和之外的其他类型Task<T>
。
- 用
- 数字文字语法改进
- 新的令牌提高了数字常量的可读性。
本主题的其余部分讨论每个功能。对于每个功能,您将了解其背后的原因。你将学习语法。您将看到一些示例场景,其中使用新功能将使您作为开发人员更有效率。
out
变量
out
在此版本中,支持参数的现有语法已得到改进。
以前,您需要将out变量及其初始化的声明分成两个不同的语句:
复制
C#
int numericResult;
if (int.TryParse(input, out numericResult))
WriteLine(numericResult);
else
WriteLine("Could not parse input");
您现在可以out
在方法调用的参数列表中声明变量,而不是编写单独的声明语句:
复制
C#
if (int.TryParse(input, out int result))
WriteLine(result);
else
WriteLine("Could not parse input");
out
为了清楚起见,您可能需要指定变量的类型,如上所示。但是,该语言支持使用隐式类型的本地变量:
复制
C#
if (int.TryParse(input, out var answer))
WriteLine(answer);
else
WriteLine("Could not parse input");
- 代码更容易阅读。
- 你声明out变量你使用它,而不是在上面的另一行。
- 无需分配初始值。
- 通过
out
在方法调用中声明变量在哪里使用,在分配之前不能意外使用它。
- 通过
这个功能的最常见的用法就是Try
模式。在此模式中,方法返回bool
指示成功或失败,以及out
如果方法成功则提供结果的 变量。
当使用out
变量声明时,声明的变量“leaks”进入if语句的外部范围。这允许您以后使用该变量:
复制
C#
if (!int.TryParse(input, out int result))
{
return null;
}
return result;
元组
C#为类和结构提供了丰富的语法,用于解释您的设计意图。但有时,丰富的语法需要额外的工作,而最小的收益。您可能经常编写需要包含多个数据元素的简单结构的方法。为了支持这些情况, 元组被添加到C#。元组是轻量级数据结构,包含多个字段来表示数据成员。字段未被验证,您无法定义自己的方法
注意
元组在C#7之前作为API可用,但有很多限制。最重要的是,这些元组的成员被命名Item1
,Item2
等等。语言支持支持元组字段的语义名称。
您可以通过将每个成员分配给值来创建一个元组:
复制
C#
var letters = ("a", "b");
该赋值创建一个元组,其成员是Item1
,并Item2
遵循现有的元组语法。您可以修改该赋值来创建一个为元组的每个成员提供语义名称的元组:
复制
C#
(string Alpha, string Beta) namedLetters = ("a", "b");
注意
新的元组功能需要System.ValueTuple
类型。对于Visual Studio 15 Preview 5及更早版本的预览版本,您必须添加NuGet软件包“System.ValueTuple”,可在预发布流中使用。
该namedLetters
元组包含称作场Alpha
和 Beta
。在元组赋值中,您还可以指定作业右侧的字段名称:
复制
C#
var alphabetStart = (Alpha: "a", Beta: "b");
该语言允许您在赋值的左侧和右侧指定字段的名称:
复制
C#
(string First, string Second) firstLetters = (Alpha: "a", Beta: "b");
上面的行生成一个警告,CS8123
,告诉你的赋值右侧的名称,Alpha
以及Beta
被忽略,因为它们与左侧的名称冲突,First
和Second
。
上面的例子显示了声明元组的基本语法。元组最适合作为返回类型private
和internal
方法。元组为这些方法提供了一个简单的语法来返回多个离散值:您保存创建a class
或struct
定义返回类型的工作。不需要创建一个新的类型。
创建元组更有效率和更高效。它是一种更简单,轻量级的语法来定义一个带有多个值的数据结构。下面的示例方法返回在整数序列中找到的最小值和最大值:
复制
C#
private static (int Max, int Min) Range(IEnumerable<int> numbers)
{
int min = int.MaxValue;
int max = int.MinValue;
foreach(var n in numbers)
{
min = (n < min) ? n : min;
max = (n > max) ? n : max;
}
return (max, min);
}
以这种方式使用元组提供了几个优点:
- 您保存创作的作品
class
或struct
定义返回的类型的作品。 - 您不需要创建新的类型。
- 语言增强功能无需调用Create <T1>(T1)方法。
该方法的声明提供了返回的元组的字段的名称。当你调用该方法,返回值是一个元组,其字段Max
和Min
:
复制
C#
var range = Range(numbers);
可能有时候要解压缩从方法返回的元组的成员。您可以通过为元组中的每个值声明单独的变量来实现。这被称为解构元组:
复制
C#
(int max, int min) = Range(numbers);
您还可以为.NET中的任何类型提供类似的解构。这是通过写一个Deconstruct
方法作为类的成员完成的。该 Deconstruct
方法为out
您要提取的每个属性提供一组参数。考虑这个Point
类,它提供一个解构方法来提取X
和Y
坐标:
复制
C#
public class Point
{
public Point(double x, double y)
{
this.X = x;
this.Y = y;
}
public double X { get; }
public double Y { get; }
public void Deconstruct(out double x, out double y)
{
x = this.X;
y = this.Y;
}
}
您可以通过为一个元组分配一个元组来提取各个字段Point
:
复制
C#
var p = new Point(3.14, 2.71);
(double X, double Y) = p;
您不受Deconstruct
方法中定义的名称的约束。您可以将提取变量重命名为作业的一部分:
复制
C#
(double horizontalDistance, double verticalDistance) = p;
模式匹配
模式匹配是一种功能,允许您在属性以外的对象的类型上实现方法分派。您可能已经熟悉基于对象类型的方法分派。在面向对象编程中,虚拟和覆盖方法提供语言语法来实现基于对象类型的方法调度。Base和Derived类提供不同的实现。模式匹配表达式扩展了这个概念,以便您可以轻松地实现与继承层次结构无关的类型和数据元素的类似的分派模式。
模式匹配支持is
表达式和switch
表达式。每个都可以检查对象及其属性来确定该对象是否满足所寻求的模式。您可以使用when
关键字为模式指定其他规则。
is
表达
所述is
图案表达延伸熟悉的is
操作者查询一个对象超出其类型。
我们从一个简单的场景开始吧。我们将为此场景添加功能,演示了模式匹配表达式如何使不起作用的类型的算法变得容易。我们将从一个计算多个模具卷总和的方法开始:
复制
C#
public static int DiceSum(IEnumerable<int> values)
{
return values.Sum();
}
您可能会很快发现,您需要找到一些模具的总和,其中一些卷筒由多个模具制成。输入序列的一部分可能是多个结果而不是单个数字:
复制
C#
public static int DiceSum2(IEnumerable<object> values)
{
var sum = 0;
foreach(var item in values)
{
if (item is int val)
sum += val;
else if (item is IEnumerable<object> subList)
sum += DiceSum2(subList);
}
return sum;
}
is
在这种情况下,模式表达式工作得很好。作为检查类型的一部分,您将编写一个变量初始化。这将创建验证的运行时类型的新变量。
当您不断扩展这些方案时,您可能会发现您构建更多的 语句if
和else if
语句。一旦变得笨拙,您可能需要切换到switch
模式表达式。
switch
语句更新
该比赛的表达有一种熟悉的语法,基于该switch
声明的C#语言中有一部分。在添加新案例之前,让我们翻译现有的代码以使用匹配表达式:
复制
C#
public static int DiceSum3(IEnumerable<object> values)
{
var sum = 0;
foreach (var item in values)
{
switch (item)
{
case int val:
sum += val;
break;
case IEnumerable<object> subList:
sum += DiceSum3(subList);
break;
}
}
return sum;
}
匹配表达式与表达式的语法略有不同is
,您可以在表达式的开头声明类型和变量case
。
匹配表达式还支持常量。这可以通过分解简单的案例节省时间:
复制
C#
public static int DiceSum4(IEnumerable<object> values)
{
var sum = 0;
foreach (var item in values)
{
switch (item)
{
case 0:
break;
case int val:
sum += val;
break;
case IEnumerable<object> subList when subList.Any():
sum += DiceSum4(subList);
break;
case IEnumerable<object> subList:
break;
case null:
break;
default:
throw new InvalidOperationException("unknown item type");
}
}
return sum;
}
上面的代码添加了0
一个例子int
,null
作为一个特殊情况,当没有输入的情况下。这表明了切换模式表达式中的一个重要的新功能:表达式的顺序case
现在很重要。的0
情况下,必须在之前一般会出现int
的情况。否则,匹配的第一个模式是这种int
情况,即使是值0
。如果您不小心对匹配表达式进行排序,以便稍后的情况已经被处理,编译器会标记并产生错误。
同样的行为使特殊情况能够为空的输入序列。您可以看到IEnumerable
具有元素的项目的情况必须出现在一般IEnumerable
情况之前。
这个版本也增加了一个default
例子。default
始终对案件进行最后评估,无论其在源中显示的顺序如何。因此,约定是把default
案件延续下去。
最后,我们来添加一个最后case
一个新的死亡风格。一些游戏使用百分位数骰子代表更大范围的数字。
注意
两个10双面百分骰子可以通过99.一种模具已标记的边表示0每一个数00
,10
,20
,... 90
。其他模具具有两侧标记0
,1
,2
,... 9
。将两个死亡值添加到一起,您可以从0到99获得每个数字。
要将这种模具添加到您的集合中,首先要定义一个类型来表示百分位数的模具:
复制
C#
public struct PercentileDie
{
public int Value { get; }
public int Multiplier { get; }
public PercentileDie(int multiplier, int value)
{
this.Value = value;
this.Multiplier = multiplier;
}
}
然后,添加case
新类型的匹配表达式:
复制
C#
public static int DiceSum5(IEnumerable<object> values)
{
var sum = 0;
foreach (var item in values)
{
switch (item)
{
case 0:
break;
case int val:
sum += val;
break;
case PercentileDie die:
sum += die.Multiplier * die.Value;
break;
case IEnumerable<object> subList when subList.Any():
sum += DiceSum5(subList);
break;
case IEnumerable<object> subList:
break;
case null:
break;
default:
throw new InvalidOperationException("unknown item type");
}
}
return sum;
}
模式匹配表达式的新语法使得使用清晰简洁的语法可以更容易地创建基于对象类型或其他属性的调度算法。模式匹配表达式使得这些构造对于与继承无关的数据类型。
参考当地人和回报
此功能启用使用和返回对其他地方定义的变量的引用的算法。一个例子是使用大型矩阵,并找到具有某些特征的单个位置。一种方法将返回矩阵中单个位置的两个索引:
复制
C#
public static (int i, int j) Find(int[,] matrix, Func<int, bool> predicate)
{
for (int i = 0; i < matrix.GetLength(0); i++)
for (int j = 0; j < matrix.GetLength(1); j++)
if (predicate(matrix[i, j]))
return (i, j);
return (-1, -1); // Not found
}
这段代码有很多问题。首先,这是一个返回一个元组的公共方法。该语言支持此功能,但用户定义的类型(类或结构体)是公共API的首选。
其次,该方法将索引返回到矩阵中的项。这导致调用者编写使用这些索引的代码来取消引用矩阵并修改单个元素:
复制
C#
var indices = MatrixSearch.Find(matrix, (val) => val == 42);
Console.WriteLine(indices);
matrix[indices.i, indices.j] = 24;
您宁愿编写一个方法,返回对 要更改的矩阵元素的引用。您只能通过使用不安全的代码并int
在以前的版本中返回一个指针来完成此操作。
我们来浏览一系列更改,以演示参考本地功能,并展示如何创建一个返回对内部存储的引用的方法。一路上,您将学习参考回报的规则和参考本地功能,保护您免受意外误用。
首先修改Find
方法声明,使其返回ref int
而不是元组。然后,修改return语句,以便返回存储在矩阵中的值,而不是两个索引:
复制
C#
// Note that this won‘t compile.
// Method declaration indicates ref return,
// but return statement specifies a value return.
public static ref int Find2(int[,] matrix, Func<int, bool> predicate)
{
for (int i = 0; i < matrix.GetLength(0); i++)
for (int j = 0; j < matrix.GetLength(1); j++)
if (predicate(matrix[i, j]))
return matrix[i, j];
throw new InvalidOperationException("Not found");
}
当您声明一个方法返回一个ref
变量时,您还必须将ref
关键字添加到每个返回语句。这通过引用返回,并帮助开发人员阅读代码,以后记住该方法返回参考:
复制
C#
public static ref int Find3(int[,] matrix, Func<int, bool> predicate)
{
for (int i = 0; i < matrix.GetLength(0); i++)
for (int j = 0; j < matrix.GetLength(1); j++)
if (predicate(matrix[i, j]))
return ref matrix[i, j];
throw new InvalidOperationException("Not found");
}
现在该方法返回对矩阵中整数值的引用,您需要修改它被调用的位置。该var
声明意味着,valItem
现在是int
不是一个元组:
复制
C#
var valItem = MatrixSearch.Find3(matrix, (val) => val == 42);
Console.WriteLine(valItem);
valItem = 24;
Console.WriteLine(matrix[4, 2]);
WriteLine
在上面的例子中的第二个声明打印出的值42
,而不是24
。变量valItem
是一个int
,而不是一个ref int
。该var
关键字使编译器能够指定类型,但不会隐式地添加ref
修饰符。相反,该值称为由ref return
被复制到变量的赋值的左手侧。该变量不是ref
本地的。
为了获得所需的结果,您需要将ref
修饰符添加到局部变量声明中,以使变量在返回值为引用时为引用:
复制
C#
ref var item = ref MatrixSearch.Find3(matrix, (val) => val == 42);
Console.WriteLine(item);
item = 24;
Console.WriteLine(matrix[4, 2]);
现在,WriteLine
上面的例子中的第二个语句将打印出该值24
,表明矩阵中的存储已被修改。局部变量已被声明为ref
修饰符,它将ref
返回。ref
声明时必须初始化一个变量,不能拆分声明和初始化。
C#语言有另外两个规则可以保护您免受ref
当地人的误用和返回:
- 您不能为
ref
变量赋值。- 这不允许这样的声明
ref int i = sequence.Count();
- 这不允许这样的声明
- 您不能返回
ref
到其生命周期不超出方法执行的变量。- 这意味着您不能返回对局部变量或类似范围的引用。
这些规则确保您不会意外混合价值变量和参考变量。他们还确保您不能有参考变量参考作为垃圾回收候选的存储。
引用本地和引用返回的添加使得通过避免复制值或执行解引用操作多次更有效的算法。
本地功能
类的许多设计包括仅从一个位置调用的方法。这些附加的私有方法保持每个方法的小小和集中。但是,第一次阅读时,他们可以更难理解课堂。必须在单个呼叫位置的上下文之外了解这些方法。
对于这些设计,本地函数使您能够在另一种方法的上下文中声明方法。这使得类的读者更容易看到本地方法只是从声明的上下文中调用。
本地函数有两个很常见的用例:public iterator方法和public异步方法。这两种类型的方法都会生成比程序员可能期望的错误报告错误的代码。在迭代器方法的情况下,只有在调用返回的序列的代码时才会观察到任何异常。在异步方法的情况下,只有Task
等待返回时才会观察到任何异常 。
我们从迭代器方法开始:
复制
C#
public static IEnumerable<char> AlphabetSubset(char start, char end)
{
if ((start < ‘a‘) || (start > ‘z‘))
throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
if ((end < ‘a‘) || (end > ‘z‘))
throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");
if (end <= start)
throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
for (var c = start; c < end; c++)
yield return c;
}
检查下面的错误调用迭代器方法的代码:
复制
C#
var resultSet = Iterator.AlphabetSubset(‘f‘, ‘a‘);
Console.WriteLine("iterator created");
foreach (var thing in resultSet)
Console.Write($"{thing}, ");
resultSet
迭代时抛出异常,而不是resultSet
创建时。在这个例子中,大多数开发人员可以快速诊断问题。然而,在较大的代码库中,创建迭代器的代码通常不会与枚举结果的代码接近。您可以重构代码,使公共方法验证所有参数,并且私有方法生成枚举:
复制
C#
public static IEnumerable<char> AlphabetSubset2(char start, char end)
{
if ((start < ‘a‘) || (start > ‘z‘))
throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
if ((end < ‘a‘) || (end > ‘z‘))
throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");
if (end <= start)
throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
return alphabetSubsetImplementation(start, end);
}
private static IEnumerable<char> alphabetSubsetImplementation(char start, char end)
{
for (var c = start; c < end; c++)
yield return c;
}
这个重构版本会立即抛出异常,因为public方法不是迭代器方法; 只有私有方法使用 yield return
语法。但是,这种重构存在潜在的问题。私有方法只能从公共接口方法调用,否则跳过所有参数验证。类的读者必须通过阅读整个类并搜索对该alphabetSubsetImplementation
方法的任何其他引用来发现这一事实。
您可以通过alphabetSubsetImplementation
在公共API方法中声明为本地函数来使设计意图更加清晰 :
复制
C#
public static IEnumerable<char> AlphabetSubset3(char start, char end)
{
if ((start < ‘a‘) || (start > ‘z‘))
throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
if ((end < ‘a‘) || (end > ‘z‘))
throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");
if (end <= start)
throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
return alphabetSubsetImplementation();
IEnumerable<char> alphabetSubsetImplementation()
{
for (var c = start; c < end; c++)
yield return c;
}
}
上面的版本清楚地表明,本地方法仅在外部方法的上下文中引用。本地函数的规则还可以确保开发人员无法从类中另一个位置意外地调用本地函数,并绕过参数验证。
同样的技术可以用于async
确保在异步工作开始之前抛出由参数验证产生的异常的方法:
复制
C#
public Task<string> PerformLongRunningWork(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));
return longRunningWorkImplementation();
async Task<string> longRunningWorkImplementation()
{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
}
}
注意
局部函数支持的一些设计也可以使用lambda表达式来完成。有兴趣的人可以阅读更多关于差异的信息
更健康的成员
C#6引入表达健全成员 成员函数,和只读属性。C#7扩展了可以实现为表达式的允许成员。在C#7,可以实现 构造函数,终结器,并get
与set
在存取性能 和索引。以下代码显示了每个的示例:
复制
C#
// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;
// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");
private string label;
// Expression-bodied get / set accessors.
public string Label
{
get => label;
set => this.label = value ?? "Default label";
}
注意
这个例子不需要一个finalizer,但它显示了演示语法。你不应该在你的类中实现一个finalizer,除非有必要释放非托管资源。您还应考虑使用 SafeHandle类,而不是直接管理非托管资源。
表达式成员的这些新位置代表了C#语言的重要里程碑:这些功能由在开源Roslyn项目上工作的社区成员实现 。
投掷表情
在C#中,throw
一直是一个声明。因为throw
是一个语句,而是一个表达式,那里有C#构造,你不能使用它。这些包括条件表达式,空合并表达式和一些lambda表达式。表达式成员的添加增加了更多的throw
表达式将有用的位置。所以你可以编写任何这些结构,C#7引入了throw表达式。
语法与您一直用于语句的语法相同throw
。唯一的区别是,现在你可以将它们放在新的位置,例如条件表达式中:
复制
C#
public string Name
{
get => name;
set => name = value ??
throw new ArgumentNullException(paramName: nameof(value), message: "New name must not be null");
}
此功能可在初始化表达式中使用throw表达式:
复制
C#
private ConfigResource loadedConfig = LoadConfigResourceOrDefault() ??
throw new InvalidOperationException("Could not load config");
以前,这些初始化将需要在一个构造函数中,在构造函数的正文中使用throw语句:
复制
C#
public ApplicationOptions()
{
loadedConfig = LoadConfigResourceOrDefault();
if (loadedConfig == null)
throw new InvalidOperationException("Could not load config");
}
注意
在构造对象期间,这两个前面的结构都将引发异常抛出。那些往往难以恢复。因此,不鼓励在施工期间抛出异常的设计。
广义异步返回类型
返回一个Task
从异步方法对象可以引入在某些路径的性能瓶颈。Task
是一个引用类型,所以使用它意味着分配一个对象。在使用async
修饰符声明的方法返回缓存结果或同步完成的情况下,额外的分配可能会在性能关键部分代码中成为重要的时间成本。如果这些分配发生在严格的循环中,则可能会变得非常昂贵。
新的语言功能意味着异步方法可能除了返回其他类型Task
,Task<T>
和void
。返回的类型必须仍然满足async模式,这意味着一种GetAwaiter
方法必须可访问。作为一个具体的例子,该ValueTask
类型已经添加到.NET框架中,以利用这种新的语言功能:
复制
C#
public async ValueTask<int> Func()
{
await Task.Delay(100);
return 5;
}
注意
您需要添加预发行版NuGet软件包System.Threading.Tasks.Extensions
才能ValueTask
在Visual Studio 15 Preview 5中使用。
简单的优化将ValueTask
在Task
以前使用的地方 使用。但是,如果要手动执行额外的优化,可以从异步工作中缓存结果,并在随后的调用中重用结果。该ValueTask
结构体具有一个带有参数的Task
构造函数,以便可以ValueTask
从任何现有异步方法的返回值构造一个:
复制
C#
public ValueTask<int> CachedFunc()
{
return (cache) ? new ValueTask<int>(cacheResult) : new ValueTask<int>(loadCache());
}
private bool cache = false;
private int cacheResult;
private async Task<int> loadCache()
{
// simulate async work:
await Task.Delay(100);
cache = true;
cacheResult = 100;
return cacheResult;
}
与所有性能建议一样,您应该对两个版本进行基准测试,然后对代码进行大规模更改。
数字文字语法改进
误读数字常量可能会使您在第一次阅读时难以理解代码。当这些数字用作位掩码或其他符号而不是数字值时,通常会发生这种情况。C#7包括两个新功能,可以更容易地以最可读的方式编写数字用于预期用途:二进制文字和数字分隔符。
在创建位掩码时,或者当数字的二进制表示形成最可读的代码时,请以二进制形式写入该数字:
复制
C#
public const int One = 0b0001;
public const int Two = 0b0010;
public const int Four = 0b0100;
public const int Eight = 0b1000;
将0b
在恒定的开头指示该数字被写成二进制数。
二进制数可以很长,所以通过引入_
一个数位分隔符来看,更容易看到位模式:
复制
C#
public const int Sixteen = 0b0001_0000;
public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;
数字分隔符可以出现在常量的任何位置。对于10号数字,通常将其用作千位分隔符:
复制
C#
public const long BillionsAndBillions = 100_000_000_000;
数字分离器可以与被使用decimal
,float
和double
类型以及:
复制
C#
public const double AvogadroConstant = 6.022_140_857_747_474e23;
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;
总而言之,您可以声明具有更多可读性的数字常量。
参考地址:https://docs.microsoft.com/zh-cn/dotnet/articles/csharp/whats-new/csharp-7