最新微网站建设价格,用flash做的ppt模板下载网站,网站地图优化,大连网站建设网站建设用 OOP 构建自己的类型
Building Your Own Types with Object-Oriented Programming
本章主题#xff1a;
讨论 OOP构建类库在字段 field 中存储数据使用方法与元组 tuple使用属性和索引器控制访问使用 object 进行模式匹配#xff08;Pattern matching#xff09;使用 r…用 OOP 构建自己的类型
Building Your Own Types with Object-Oriented Programming
本章主题
讨论 OOP构建类库在字段 field 中存储数据使用方法与元组 tuple使用属性和索引器控制访问使用 object 进行模式匹配Pattern matching使用 record 类型
讨论 OOP
在 C# 中使用关键字 class、record、struct 来定义一个 object 的类型。
OOP 的概念如下
Encapsulation 封装即与对象相关的数据和操作的组合。Composition 组合是关于对象是由什么组成的。Aggregation 聚合是关于可以与对象组合的东西。Inheritance 继承指通过从基类或超类派生子类来重用代码。Abstraction 抽象指捕捉对象的核心思想并忽略细节 details 或具体 specifics。C# 具有abstract 关键字来形式化这个概念但不要将抽象概念与 abstract 关键字的使用含义混淆因为它的含义不止于此。抽象的概念也可以使用接口来实现。如果一个类不是明确抽象的那么它可以被描述为具体的concrete。基类或超类通常是抽象的Polymorphism 多态性是指允许派生类重写继承的操作以提供自定义行为。
构建类库
Building class libraries
类库程序集将类型组合成易于部署的单元DLL 文件。为了使您编写的代码可在多个项目中重用您应该将其放入类库程序集中就像 Microsoft 所做的那样。
我们创建一个类库项目
mkdir Chapter05
cd Chapter05
dotnet new sln -n Chapter05
dotnet new classlib -n PacktLibraryNetStandard2我们注意到项目的 .csproj 文件中还有目标框架版本 TargetFramework
PropertyGroupTargetFrameworknet7.0/TargetFrameworkImplicitUsingsenable/ImplicitUsingsNullableenable/Nullable
/PropertyGroup我们可以进行相关修改
更改目标 .NET Standard 2.0指定 C# 版本为 12并为所有文件静态引入 System.Console 类
Project SdkMicrosoft.NET.Sdk
PropertyGroup!--.NET Standard 2.0 class library can be used by:.NET Framework, Xamarin, modern .NET. --TargetFrameworknetstandard2.0/TargetFramework!--Compile this library using C# 12 so we can use mostmodern compiler features. --LangVersion12/LangVersionNullableenable/NullableImplicitUsingsenable/ImplicitUsings
/PropertyGroup
ItemGroupUsing IncludeSystem.Console Statictrue /
/ItemGroup
/Project… 尽管我们使用了 C#12 编译器但是一些现代编译器特性还需要一个现代 .NET 运行时。 … 良好实践要使用所有最新的 C# 语言和 .NET 平台功能请将类型放入 .NET 8 类库中。为了支持 .NET Core、.NET Framework 和 Xamarin 等旧版 .NET 平台请将可能重用的类型放入 .NET Standard 2.0 类库中。默认情况下面向 .NET Standard 2.0 使用 C# 7 编译器但这可以被覆盖因此即使您仅限于 .NET Standard 2.0 API您也可以获得较新的 SDK 和编译器的优势。 …
文件域名称空间
以往我们定义一个类型如类将其嵌套在一个名称空间中如下所示
namespace Packt.Shared
{public class Person{}
}如果要在同一个文件中顶一个多个类型而这些类型在不同的名称空间中则要使用大括号将这些类型分别包含在对应的名称空间的大括号中。
C#10 引入了文件域的名称空间我们只需要声明一个名称空间则对整个文件都有效
namespace Packt.Shared;
public class Person
{
}这就是 file-scoped namespace文件域名称空间
在一个名称空间中定义一个类
Defining a class in a namespace
我们在类库项目中定义一个文件为 Person.cs 内容
namespace Packt.Shared;
public class Person
{
}注意为了使该类可以被访问使用了 public 关键字。
类型访问修饰符
Understanding type access modifiers
注意到上述代码中 class 前的 public 关键字这就是一个访问修饰符access modifier它允许其来自类库外的其他任何代码能够访问这个类。
如果不显式应用 public 关键字则只能在定义它的程序集assembly中访问它。这是因为类的隐式访问修饰符是 internal。我们需要这个类可以在程序集外部访问所以我们必须确保它是公共的。
如果是嵌套类即在一个类 A 中定义另一个类 B则内部这个类 B 的访问修饰符是 private这意味着不可以在类 A 以外的地方访问。
.NET 7 引入了文件访问修饰符 file这意味着该类型只能在本文件中使用 良好实践类的两个最常见的访问修饰符是 public 和 internal类的默认访问修饰符。始终显式指定类的访问修饰符以明确它是什么。其他访问修饰符包括 private 和 file 很少被使用。 成员
Understanding members
成员可以是 字段、方法或两者的特殊版本、具体描述如下
**字段field**被用来存储数据。可以认为字段是某种类型的变量。有三种字段
常量constant该数据从未改变。编译器实际上将数据复制到任何读取它的代码中。只读read-only该类被实例化后只读数据就不能被更改但是只读数据可以在实例化时从而外的来源计算或加载。事件Event该数据引用一个或多个方法这些方法将在某些事情发生时执行。比如点击按钮或接收请求。
方法Method用于执行语句。有四种方法
构造器constructor将会在使用 new 关键字来申请内存来实例化一个类时执行。属性property当设置set或读取get数据时。数据往往被存储在一个字段中但也可以存储在外部或在运行时计算。除非需要公开字段的内存地址否则属性是封装encapsulate字段的首选方式例如使用 Console.ForegroundColor 设置控制台应用程序中文本的当前颜色。索引indexer用于使用语法 [] 时获取或设置数据。操作符operator就是操作符重载
导入一个名称空间来使用一个类型
如果我们想使用类库中的类型需要引用该类库项目。
我们创建一个 控制台项目
dotnet new console -n PeopleApp
dotnet sln add PeopleApp修改项目的 .csproj 文件以引入 Person 类型
Project SdkMicrosoft.NET.Sdk
PropertyGroupOutputTypeExe/OutputTypeTargetFrameworknet8.0/TargetFrameworkNullableenable/NullableImplicitUsingsenable/ImplicitUsings
/PropertyGroupItemGroupProjectReference Include../PacktLibraryNetStandard2/PacktLibraryNetStandard2.csproj /
/ItemGroup
ItemGroupUsing IncludeSystem.Console Statictrue /
/ItemGroup
/Project关注
ItemGroup
ProjectReference Include../PacktLibraryNetStandard2/PacktLibraryNetStandard2.csproj /
/ItemGroup然后构建即可
dotnet build在控制台项目 PeopleApp 中添加一个类文件 Program.Helpers.cs内容如下
using System.Globalization; // To use CultureInfo.partial class Program
{private static void ConfigureConsole(string culture en-US,bool useComputerCulture false,bool showCulture true){OutputEncoding System.Text.Encoding.UTF8;if (!useComputerCulture){CultureInfo.CurrentCulture CultureInfo.GetCultureInfo(culture);}if (showCulture){System.Console.WriteLine($Current culture: {CultureInfo.CurrentCulture.DisplayName}.);}}
}…
然后再 Program.cs 中实例化类并调用上述方法
using Packt.Shared;ConfigureConsole();// Alternatives:
// ConfigureConsole(useComputerCulture: true); // Use your culture.
// ConfigureConsole(culture: fr-FR); // Use French culture.// Person bob new Person(); // C# 1 or later.
// var bob new Person(); // C# 3 or later.
Person bob new(); // C# 9 or later.
WriteLine(bob); // Implicit call to ToString().
// WriteLine(bob.ToString()); // Does the same thing.…
运行结果
Current culture: 英语美国.
Packt.Shared.Person…
为什么一个空的类型都有一个 ToString 的方法
继承自 System.Object
尽管我们的自定义类没有显式指定继承自哪一个类型但是所有的类型最终都直接或间接地继承自一个名为 System.Object 的特殊类型。该类型的 ToString 方法实现输出完整的名称空间和类型名。
如果想指明一个类型显式地继承
public class Person: System.Object如果一个 类B 继承自 类A 我们称 类A 为基类base 或 superclass而 类B 为派生类或子类the derived or subclass。
其实 System.Object 等同于 C#关键字 object。 应该知道所有的类都隐式地直接继承或间接继承自 object 使用别名避免名称空间冲突
Avoiding a namespace conflict with a using alias
可能有两个名称空间包含相同的类型名称导入这两个名称空间会导致歧义。例如JsonOptions 存在于多个 Microsoft 定义的命名空间中。如果你使用错误的配置来配置 JSON 序列化那么它将被忽略你会很困惑为什么
一个例子
// In the file, France.Paris.cs
namespace France
{public class Paris{}
}
// In the file, Texas.Paris.cs
namespace Texas
{public class Paris{}
}// In the file, Program.cs
using France;
using Texas;
Paris p new();我们编译时会报错
Error CS0104: Paris is an ambiguous reference between France.Paris and Texas.Paris我们可以定义别名如下所示
using France; // To use Paris.
using Tx Texas; // Tx 为名称空间地别名但是并未导入
Paris p1 new(); // Creates an instance of France.Paris.
Tx.Paris p2 new(); // Creates an instance of Texas.Paris.使用别名重命名类型
当我们想要重命名一个类型时也可以使用别名。类似于 Python 的 as 可以减少指定名称空间时的冗长名字
using Env System.Environment;
WriteLine(Env.OSVersion);
WriteLine(Env.MachineName);
WriteLine(Env.CurrentDirectory);自 C#12 之后我们可以给任意类型别名。这意味着可以重命名已存在的类型或给一个无命名*unnamed的类型如 tuple 一个类型名。
在字段中存储数据
Storing data in fields
定义字段
Defining fields
在 Person 类中我们定义两个公有字段
public class Person : object
{#region Fields: Data or state for this person.public string? Name; // ? means it can be null.public DateTimeOffset Born;#endregion
}… 关于出生日期的类型有许多选择。.NET6 引入了 DateOnly 类型该类型只保存日期而没有时间。DateTime 存储日期和时间但它在本地时间和 UTC 时间之间有所不同。最佳选择是 DateTimeOffset该类型存储偏离 Universal Coordinated Time (UTC)与时区有关 的日期、时间和小时。 选择合适的。 字段的类型
从 C# 8 开始编译器能够在引用类型如字符串具有 null 值并因此可能引发 NullReferenceException 时向您发出警告。从 .NET 6 开始SDK 默认启用这些警告。您可以在字符串类型后加上问号 ? 来表示您接受这一点然后警告就会消失。
成员访问修饰符
Member access modifiers
封装的一部分含义就是指控制成员能否被其他代码可见。
值得注意的是默认的成员访问修饰符是 private。
有四种成员访问修饰符 member access modifier 关键字以及两种访问修饰符关键字的组合可以用于一个类的成员字段或方法。成员访问修饰符应用于独立个体。它们与应用于整个类型的类型访问修饰符相似但独立。
六种可能的组合如下所示
私有 private 默认该成员只能在类型内部被访问。内部 interval该成员可以在类型内部或相同程序集内的其他任意类型访问。保护 protected该成员可以在类型内部或派生类型中被访问。公有 public该类型可以被任意访问。内部保护 internal protected该成员可以在类型内部、相同程序集内的其他任意类型以及派生类型内访问。理解为 internal_or_protected私有保护 private protected该成员可以在类型内部、相同程序集内的派生类型内访问。理解为 internel_and_proteced该组合自 C#7.2 引入。
… 良好实践将访问修饰符之一显式应用于所有类型成员即使您想对成员使用私有的隐式访问修饰符。此外字段通常应该是私有的或受保护的然后您应该创建公共属性property来获取或设置字段值。这是因为该属性随后控制访问。 …
设置或访问字段值
Setting and outputting field values
代码
bob.Name Bob Smith;
bob.Born new DateTimeOffset(year: 1965, month: 12, day: 22,hour: 16, minute: 28, second: 0,offset: TimeSpan.FromHours(-5)); // US Eastern 标准时间WriteLine(format: {0} was born on {1:D}., // Long date.
arg0: bob.Name, arg1: bob.Born);… arg1 的格式代码是标准日期和时间格式之一。 D 表示长日期格式d 表示短日期格式。您可以通过以下链接了解有关标准日期和时间格式代码的更多信息 https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings。 运行结果
Bob Smith was born on Wednesday, December 22, 1965.…
使用初始化语法设置字段值
Setting field values using object initializer syntax
还可以使用带有大括号的简写对象初始化语法来初始化字段该语法是在 C# 3 中引入的
Person alice new()
{Name Alice Jones,Born new(1998, 3, 7, 16, 28, 0,// This is an optional offset from UTC time zone.TimeSpan.Zero)
};
WriteLine(format: {0} was born on {1:d}., // Short date.arg0: alice.Name, arg1: alice.Born);运行结果
Alice Jones was born on 3/7/1998.… 良好实践使用命名参数来传递参数这样值的含义就更清楚特别是对于像 DateTimeOffset 这样的类型其中有一堆相继的数字。 …
使用枚举类型存储值
Storing a value using an enum type
定义枚举。我们在类库项目中增加一个新的文件 WondersOfTheAncientWorld.cs并在其中定义枚举
namespace Packt.Shared;
public enum WondersOfTheAncientWorld
{GreatPyramidOfGiza,HangingGardensOfBabylon,StatueOfZeusAtOlympia,TempleOfArtemisAtEphesus,MausoleumAtHalicarnassus,ColossusOfRhodes,LighthouseOfAlexandria
}…
使用枚举。在类库项目的 Person.cs 中使用枚举类型来定义一个字段
public WondersOfTheAncientWorld FavoriteAncientWonder;在引用该类库项目的控制台应用中使用该字段
bob.FavoriteAncientWonder WondersOfTheAncientWorld.StatueOfZeusAtOlympia;
WriteLine(format: {0}s favorite wonder is {1}. Its integer is {2}.,arg0: bob.Name,arg1: bob.FavoriteAncientWonder,arg2: (int)bob.FavoriteAncientWonder);运行结果
Bob Smiths favorite wonder is StatueOfZeusAtOlympia. Its integer is 2.为了提高效率枚举值在内部存储为 int。 int 值从 0 开始自动分配因此我们枚举中的第三世界奇迹的值为 2。您可以将枚举中没有列出的 int 值赋给枚举变量。它们将输出为 int 值而不是名称因为找不到匹配项。
使用一个枚举类型存储多个值
Storing multiple values using an enum type
如果我们想表达多个枚举值可以使用类似C语言的位运算的技术而不是使用一个列表。
称之为 enum flags。
我们需要使用 [Flags] 特性来修饰枚举定义并制定每个枚举的具体值与类型。我们将之前的枚举定义修改如下
namespace Packt.Shared;
[Flags]
public enum WondersOfTheAncientWorld : byte
{None 0b_0000_0000, // i.e. 0GreatPyramidOfGiza 0b_0000_0001, // i.e. 1HangingGardensOfBabylon 0b_0000_0010, // i.e. 2StatueOfZeusAtOlympia 0b_0000_0100, // i.e. 4TempleOfArtemisAtEphesus 0b_0000_1000, // i.e. 8MausoleumAtHalicarnassus 0b_0001_0000, // i.e. 16ColossusOfRhodes 0b_0010_0000, // i.e. 32LighthouseOfAlexandria 0b_0100_0000 // i.e. 64
}我们为每种选择提供了显式的值这些值在具体的 bit 位上不会重叠。
我们还应该用 System.Flags 属性来装饰 enum 类型他可以自动匹配为 逗号分隔的、枚举名称组成的字符串而不是返回 int 值。
通常枚举类型在内部使用 int 变量但由于我们不需要那么大的值因此可以通过指定类型来避免内存浪费。
该枚举可以这么用
bob.FavoriteAncientWonder WondersOfTheAncientWorld.HangingGardensOfBabylon| WondersOfTheAncientWorld.MausoleumAtHalicarnassus;
// bob.BucketList (WondersOfTheAncientWorld)18;
WriteLine(${bob.Name}s bucket list is {bob.BucketList}.);运行输出
Bob Smiths bucket list is HangingGardensOfBabylon, MausoleumAtHalicarnassus.…
使用集合存储多个值
Storing multiple values using collections
在类库的 Person.cs 文件中添加列表字段
public ListPerson Children new();注意这里要申请内存进行实例化否则该字段默认初始化为 null。
理解泛化集合
Understanding generic collections
这里注意到类似与 C 模板的语法这是 C#2 引入的。这是一个使集合强类型化Strongly typed的奇特术语也就是说编译器明确知道集合中可以存储什么类型的对象。泛型可以提高代码的性能和正确性。 强类型Strongly typed与静态类型statically typed具有不同的含义。旧的 System.Collection 类型是静态类型的以包含弱类型weakly typed的 System.Object 项。较新的 System.Collection.Generic 类型是静态类型以包含强类型strongly typed T 实例。 讽刺的是术语泛型意味着我们可以使用更具体的静态类型
我们可以这样使用上述的列表字段
// Works with all versions of C#.
Person alfred new Person();
alfred.Name Alfred;
bob.Children.Add(alfred);
// Works with C# 3 and later.
bob.Children.Add(new Person { Name Bella });
// Works with C# 9 and later.
bob.Children.Add(new() { Name Zoe });
WriteLine(${bob.Name} has {bob.Children.Count} children:);
for (int childIndex 0; childIndex bob.Children.Count; childIndex)
{WriteLine($ {bob.Children[childIndex].Name});
}静态字段
Making a field static
有时想要定义一个所有实例都共享的字段即静态成员。在成员定义前加上 static 关键字即可。
注意字段并不是唯一可以是静态的成员。构造函数、方法、属性和其他成员也可以是静态的。
常量字段
Making a field constant 使用 const 关键字定义一个从不会改变的字段并在编译时期赋予一个字面值。相关错误将会在编译器产生错误。 良好实践由于两个重要原因常量并不总是最佳选择该值必须在编译时已知并且必须可以表示为文字字符串、布尔值或数值。对 const 字段的每个引用都会在编译时替换为文字值因此如果该值在未来版本中发生更改并且您不重新编译任何引用该字段的程序集来获取新值则该值在其他项目中不会反映出改变。 只读字段
Making a field read-only
通常对于一个不会改变的字段有一个更好的选择即将其设为只读属性。使用的关键字是 readonly 。 良好实践使用只读字段而不是常量字段有两个重要原因该值可以在运行时计算或加载并且可以使用任何可执行语句来表达。因此可以使用构造函数或字段赋值来设置只读字段。对只读字段的每个引用都是实时引用因此将来的任何更改都将由调用代码正确反映。 实例化时需要设置的字段
Requiring fields to be set during instantiation
C#11 引入了 required 修饰符。将其用于一个字段field或属性property时编译器将确保在实例化时设置对应的字段或属性。这需要至少 .NET7 及之后的版本。
如果字段是可空的那么加上 required 修饰符后即使要设置为空也要显式地赋 null。
使用构造器初始化字段
Initializing fields with constructors
字段通常需要在运行时初始化您可以在构造函数中执行此操作当使用 new 关键字创建类的实例时将调用构造函数。构造函数在使用该类型的代码设置任何字段之前执行。
例如
// Read-only fields: Values that can be set at runtime.
#region Fiedls: part of fields.
public readonly string HomePlanet Earth;
public readonly DateTime Instantiated;
#endregion#region Constructors: Called when using new to instantiate a type.
public Person()
{// Constructors can set default values for fields// including any read-only fields like Instantiated.Name Unknown;Instantiated DateTime.Now;
}
#endregion…
一个类型可以有多个构造函数我们还可以再定义第二个构造函数并加上一些参数
public Person(string initialName, string homePlanet)
{Name initialName;HomePlanet homePlanet;Instantiated DateTime.Now;
}…
可以使用构造函数来设置那些 required 字段。
例如
public class Book
{// Needs .NET 7 or later as well as C# 11 or later.public required string? Isbn;public required string? Title;// Works with any version of .NET.public string? Author;public int PageCount// Constructor for use with object initializer syntax.public Book() { }// Constructor with parameters to set required fields.public Book(string? isbn, string? title){Isbn isbn;Title title;}
}注意这里的对象初始化语法object initializer syntax。我们接着看以下代码
Book book new(isbn: 978-1803237800,title: C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals)
{Author Mark J. Price,PageCount 821
};这里先使用了构造函数然后使用 对象初始化语法 设置了其他 非 reuqired 属性。
但是注意这样还会看到编译器错误因为编译器无法自动判断调用构造函数将设置两个必需的属性。
我们需要导入代码分析相关的名称空间来告诉编译器这一信息
using System.Diagnostics.CodeAnalysis; // To use [SetsRequiredMembers].
namespace Packt.Shared;
public class Book
{public Book() { } // For use with initialization syntax.[SetsRequiredMembers]public Book(string isbn, string title)…
现在就不会出现编译器错误了。 More Information: You can learn more about required fields and how to set them using a constructor at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required. 使用方法和元组
就像 C 一样。
C# 也可以重载函数一样有函数签名method signature的概念。
类似地也有默认参数可选参数optional parameters
.
命名参数值
不同的是C# 可以在调用函数时命名参数值。命名传递
Naming parameter values when calling methods
调用方法时可选参数通常与命名参数结合使用因为命名参数允许以与声明方式不同的顺序传递值。
例子
public string OptionalParameters(string command Run!, double number 0.0, bool active true)
{return string.Format(format: command is {0}, number is {1}, active is {2},arg0: command,arg1: number,arg2: active);
}调用时可以
WriteLine(bob.OptionalParameters());
WriteLine(bob.OptionalParameters(Jump!, 98.5));
WriteLine(bob.OptionalParameters(number: 52.7, command: Hide!));
// 混合调用方式
WriteLine(bob.OptionalParameters(Poke!, active: false));第三个使用命名参数并没有按照参数声明的顺序传递第四行调用第一个参数使用了位置传递的方式第二个参数使用了命名传递。 良好实践虽然您可以混合命名参数值和位置参数值但大多数开发人员更喜欢阅读在同一方法调用中使用统一调用方式的代码。 混合可选和必须参数
Mixing optional and required parameters
与C类似的是函数定义时必须参数必须在可选参数之前。
例如
public string OptionalParameters(int count,string command Run!,double number 0.0, bool active true)调用时必须传入第一个参数。但是我们仍可以使用命名传递的方式传递参数位置可变了不一定非要在可选参数之前
WriteLine(bob.OptionalParameters(3));
WriteLine(bob.OptionalParameters(3, Jump!, 98.5));
WriteLine(bob.OptionalParameters(3, number: 52.7, command: Hide!));
WriteLine(bob.OptionalParameters(3, Poke!, active: false));
bob.OptionalParameters(number: 52.7, command: Hide!,count: 3).…
控制如何传递参数
Controlling how parameters are passed
有几种方式控制如何传入参数
值传递默认被视作是 in-only。作为一个输出参数out被视作是 out-only。out 参数不能有默认值并且不能保持未初始化状态。它们必须在方法内部设置否则编译器会报错。其实也是引用引用传递ref被视作是 in-and-out。ref 参数也不能有默认值但是他们不一定必须在方法设置。作为一个输入参数in被视作是 read-only 的引用参数。in 参数的值无法更改。 在 C#7 之后因为 out 参数总会被替换所以可以在调用方法传递参数的同时定义该变量可以看之后的例子。 例子
public void PassingParameters(int w, in int x, ref int y, out int z)
{// out parameters cannot have a default and they// must be initialized inside the method.z 100;// Increment each parameter except the read-only x.w;// x; // Gives a compiler error!y;z;WriteLine($In the method: w{w}, x{x}, y{y}, z{z});
}…
int a 10;
int b 20;
int c 30;
int d 40;
WriteLine($Before: a{a}, b{b}, c{c}, d{d});
bob.PassingParameters(a, b, ref c, out d);
WriteLine($After: a{a}, b{b}, c{c}, d{d});int e 50;
int f 60;
int g 70;
WriteLine($Before: e{e}, f{f}, g{g}, h doesnt exist yet!);
// Simplified C# 7 or later syntax for the out parameter.
bob.PassingParameters(e, f, ref g, out int h);
WriteLine($After: e{e}, f{f}, g{g}, h{h});输出
Before: a10, b20, c30, d40
In the method: w11, x20, y31, z101
After: a10, b20, c31, d101
Before: e50, f60, g70, h doesnt exist yet!
In the method: w51, x60, y71, z101
After: e50, f60, g71, h101…
引用返回
Understanding ref returns
在 C# 7 或更高版本中ref 关键字不仅仅用于将参数传递给方法它还可以应用于返回值。这允许外部变量引用内部变量并在方法调用后修改其值。
这在高级场景中可能很有用例如将占位符传递到大数据结构中但这超出了本书的范围。如果您有兴趣了解更多信息可以阅读以下链接https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref#reference-return-values.
使用元组返回多个值
Combining multiple returned values using tuples
元组是将两个或多个值组合成一个单元的有效方法。
直到 2017 年的 C# 7C# 才添加了对使用括号字符 () 的元组的语言语法支持同时 .NET 添加了新的 System.ValueTuple 类型该类型在一些常见场景中比旧的 .NET System.Tuple类型更高效。
例子
定义一个返回元组的函数方法
// Method that returns a tuple: (string, int).
public (string, int) GetFruit()
{return (Apples, 5);
}使用该函数获取返回值后元组的元素自动用 Item1 和 Item2 表示
(string, int) fruit bob.GetFruit();
WriteLine(${fruit.Item1}, {fruit.Item2} there are.);为元组的字段命名
Naming the fields of a tuple
为了访问元组的字段默认的名称为 Item1、Item2等。
也可以显式指定字段名称
// Method that returns a tuple with named fields.
public (string Name, int Number) GetNamedFruit()
{return (Name: Apples, Number: 5);
}使用
var fruitNamed bob.GetNamedFruit();
WriteLine($There are {fruitNamed.Number} {fruitNamed.Name}.);这里我们使用 var 来代替完整的元组类型上述第一行代码等价于
string Name, int Number) fruitNamed bob.GetNamedFruit();…
如果直接从其他对象构建一个元组而不是作为函数返回我们可以使用 C#7.1 引入的特征元组命名引用tuple name inference。
var thing1 (Neville, 4);
WriteLine(${thing1.Item1} has {thing1.Item2} children.);
var thing2 (bob.Name, bob.Children.Count);
WriteLine(${thing2.Name} has {thing2.Count} children.);在 C#7 时这两个字段应该使用 Item1和 Item2 表示在 C#7.1及之后上述代码中 thing2 的这两个字段会自动推断并命名为 Name 和 Count。
元组别名
Aliasing tuples
在 C#12 之后也可以像 C 那样使用 using 对类型设置别名
// Aliasing a tuple type.
using UnnamedParameters (string, int);
// Aliasing a tuple type with parameter names.
using Fruit (string Name, int Number);…
解构元组
Deconstructing tuples
类似于 C 的结构化绑定但是可以指定类型
还可以将元组解构为单独的变量。解构声明与命名字段元组具有相同的语法但没有元组的命名变量如以下代码所示
// 使用两个命名字段的元组存储返回值
(string name, int number) namedFields bob.GetNamedFruit();
// You can then access the named fields.
WriteLine(${namedFields.name}, {namedFields.number});
// 直接将返回的元组解构为两个变量
(string name, int number) bob.GetNamedFruit();
// You can then access the separate variables.
WriteLine(${name}, {number});…
使用元组解构其他类型
Deconstructing other types using tuples
并不是只有元组类型才能被解构。任何含有 Deconstruct 方法的类型都可以将对象分解为不同的部分。Deconstruct 方法没有返回值void分解的每个部分通过 out 参数传递。
例子之前的 Person 类
// Deconstructors: Break down this object into parts.
public void Deconstruct(out string? name,out DateTimeOffset dob)
{name Name;dob Born;
}
public void Deconstruct(out string? name,out DateTimeOffset dob,out WondersOfTheAncientWorld fav)
{name Name;dob Born;fav FavoriteAncientWonder;
}使用时
//隐式调用 Deconstruct 方法
var (name1, dob1) bob;
WriteLine($Deconstructed person: {name1}, {dob1});
var (name2, dob2, fav2) bob;
WriteLine($Deconstructed person: {name2}, {dob2}, {fav2});…
当将对象分配给元组变量时它会被隐式调用。
使用本地函数实现功能
Implementing functionality using local functions
C# 7 引入的一个语言特性是定义本地函数local function。
本地函数是等同于本地变量的方法也就是说它们只能在定义它们的方法内部被访问。
在其他语言中被称为嵌套或内部函数nested or inner functions。
本地函数可以在一个方法的内部任意地方定义。
例子
甚至可以在返回语句之后定义本地函数
// Method with a local function.
public static int Factorial(int number)
{if (number 0){throw new ArgumentException(${nameof(number)} cannot be less than zero.);}return localFactorial(number);int localFactorial(int localNumber) // Local function.{if (localNumber 0) return 1;return localNumber * localFactorial(localNumber - 1);}
}…
使用 partial 分离类
Splitting classes using partial
将一个类在不同的文件进行定义使用 partial 关键字。
注意类的每个部分都要使用该关键字。
使用属性或索引器控制访问
Controlling access with properties and indexers
**属性property**只是一个方法或一对方法当您想要获取或设置值时它的行为和看起来像一个字段但它的行为像一个方法从而简化了语法并启用了功能functionality例如当您设置并获取值时进行验证和计算。
字段field和属性property之间的根本区别在于字段为数据提供内存地址。您可以将该内存地址传递给外部组件例如 Windows API C 风格的函数调用然后它可以修改数据。属性不为其数据提供内存地址而是提供了更多控制。您所能做的就是要求属性获取或设置数据。然后该属性执行语句并可以决定如何响应包括拒绝请求
定义只读属性
Defining read-only properties
只读属性只有 get 实现。
例子
#region Properties: Methods to get and/or set data or state
// A readonly property defined using C# 1 to 5 syntax.
public string Origin
{get{return string.Format({0} was born on {1}.,arg0: Name, arg1: HomePlanet);}
}
// C# 6 or later 定义只读属性的语法
// lambda expression body syntax.
public string Greeting ${Name} says Hello!;
public int Age DateTime.Today.Year - Born.Year;
#endregion…
使用
Person sam new()
{Name Sam,Born new(1969, 6, 25, 0, 0, 0, TimeSpan.Zero)
};
WriteLine(sam.Origin);
WriteLine(sam.Greeting);
WriteLine(sam.Age);运行结果
Sam was born on Earth
Sam says Hello!
54…
定义可设置属性
Defining settable properties
要定义可设置属性必须使用旧语法提供一对函数分别为 get 和 set。
getter 和 setter
例如
// A read-write property defined using C# 3 auto-syntax.
public string? FavoriteIceCream { get; set; }尽管您没有手动创建一个字段来存储该数据编译器将自动创建。
有时您需要对设置属性时发生的情况进行更多控制。在这种情况下您必须使用更详细的语法并手动创建私有字段来存储属性的值。
添加语句来定义私有字符串字段称为支持字段
// A private backing field to store the property value.
private string? _favoritePrimaryColor;并根据此私有字段定义属性
// A public property to read and write to the field.
public string? FavoritePrimaryColor
{get{return _favoritePrimaryColor;}set{switch (value?.ToLower()){case red:case green:case blue:_favoritePrimaryColor value;break;default:throw new ArgumentException(${value} is not a primary color. Choose from: red, green, blue.);}}
}… 良好实践避免向 getter 和 setter 添加过多代码。这可能表明您的设计存在问题。考虑添加私有方法然后在 set 和 get 方法中调用这些方法以简化实现。 …
sam.FavoriteIceCream Chocolate Fudge;
WriteLine($Sams favorite ice-cream flavor is {sam.FavoriteIceCream}.);
string color Red;
try
{sam.FavoritePrimaryColor color;WriteLine($Sams favorite primary color is {sam.FavoritePrimaryColor}.);
}
catch (Exception ex)
{WriteLine(Tried to set {0} to {1}: {2},nameof(sam.FavoritePrimaryColor), color, ex.Message);
}… 养成习惯记得异常处理。 良好实践当您想要在读取或写入字段期间执行语句而不使用方法对如 GetAge 和 SetAge时请使用属性而不是字段。
限制标志枚举值
Limiting flags enum values
对之前的公有的枚举字段更改为设置一个私有的枚举字段和一个公有属性如下所示
private WondersOfTheAncientWorld _favoriteAncientWonder;
public WondersOfTheAncientWorld FavoriteAncientWonder
{get { return _favoriteAncientWonder; }set{string wonderName value.ToString();if (wonderName.Contains(,)){throw new ArgumentException(message: Favorite ancient wonder can only have a single enum value.,paramName: nameof(FavoriteAncientWonder));}if (!Enum.IsDefined(typeof(WondersOfTheAncientWorld), value)){throw new ArgumentException(${value} is not a member of the WondersOfTheAncientWorld enum.,paramName: nameof(FavoriteAncientWonder));}_favoriteAncientWonder value;}
}我们可以通过仅检查原始枚举中是否定义了该值来简化验证因为 IsDefined 对于多个值和未定义的值返回 false。但是我想为多个值显示不同的异常因此我将使用以下事实格式化为字符串的多个值将在名称列表中包含逗号。这也意味着我们必须在检查值是否已定义之前检查多个值。但是注意逗号分隔列表是将多个枚举值表示为字符串的方式但不能使用逗号来设置多个枚举值。你应该使用 | 按位或。
定义索引器
Defining indexers
索引器Indexers允许使用数组语法来访问一个属性例如 string 允许使用索引器访问每个字符如
string alphabet abcdefghijklmnopqrstuvwxyz;
char letterF alphabet[5]; // 0 is a, 1 is b, and so on.…
索引器的参数并不一定是整数也可以是字符串或其他类。
例如
#region Indexers: Properties that use array syntax to access them.
public Person this[int index]
{get{return Children[index]; // Pass on to the ListT indexer.}set{Children[index] value;}
}// A read-only string indexer.
public Person this[string name]
{get{return Children.Find(p p.Name name);}
}
#endregion…
使用
sam.Children.Add(new() { Name Charlie,Born new(2010, 3, 18, 0, 0, 0, TimeSpan.Zero) });
sam.Children.Add(new() { Name Ella,Born new(2020, 12, 24, 0, 0, 0, TimeSpan.Zero) });// Get using Children list.
WriteLine($Sams first child is {sam.Children[0].Name}.);
WriteLine($Sams second child is {sam.Children[1].Name}.);
// Get using the int indexer.
WriteLine($Sams first child is {sam[0].Name}.);
WriteLine($Sams second child is {sam[1].Name}.);
// Get using the string indexer.
WriteLine($Sams child named Ella is {sam[Ella].Age} years old.);运行结果
Sams first child is Charlie.
Sams second child is Ella.
Sams first child is Charlie.
Sams second child is Ella.
Sams child named Ella is 3 years old.使用对象进行模式匹配
Pattern matching with objects
在此示例中我们将定义一些代表航班上各种类型乘客的类然后我们将使用具有模式匹配的 switch 表达式来确定他们的航班费用
代码
namespace Packt.Shared;
public class Passenger
{public string? Name { get; set; }
}public class BusinessClassPassenger : Passenger
{public override string ToString(){ return $Business Class: {Name};}
}
public class FirstClassPassenger : Passenger
{public int AirMiles { get; set; }public override string ToString(){return $First Class with {AirMiles:N0} air miles: {Name};}
}public class CoachClassPassenger : Passenger
{public double CarryOnKG { get; set; }public override string ToString(){return $Coach Class with {CarryOnKG:N2} KG carry on: {Name};}
}定义一个对象数组包含五个不同类型和属性值的乘客然后枚举他们输出他们的航班费用如下代码所示
// An array containing a mix of passenger types.
Passenger[] passengers {new FirstClassPassenger { AirMiles 1_419, Name Suman },new FirstClassPassenger { AirMiles 16_562, Name Lucy },new BusinessClassPassenger { Name Janice },new CoachClassPassenger { CarryOnKG 25.7, Name Dave },new CoachClassPassenger { CarryOnKG 0, Name Amit },
};
foreach (Passenger passenger in passengers)
{decimal flightCost passenger switch{FirstClassPassenger p when p.AirMiles 35_000 1_500M,FirstClassPassenger p when p.AirMiles 15_000 1_750M,FirstClassPassenger _ 2_000M,BusinessClassPassenger _ 1_000M,CoachClassPassenger p when p.CarryOnKG 10.0 500M,CoachClassPassenger _ 650M,_ 800M};WriteLine($Flight costs {flightCost:C} for {passenger});
}值得注意的是
为了访问对象的属性我们需要命名一个局部变量如上面的 p可以用于后面的表达式。如果仅仅是匹配类型我们使用 _ 抛弃该局部变量。switch 表达式的默认分支使用 _ 表示
运行结果
Flight costs $2,000.00 for First Class with 1,419 air miles: Suman
Flight costs $1,750.00 for First Class with 16,562 air miles: Lucy
Flight costs $1,000.00 for Business Class: Janice
Flight costs $650.00 for Coach Class with 25.70 KG carry on: Dave
Flight costs $500.00 for Coach Class with 0.00 KG carry on: Amit…
C#9 之后的增强模式匹配
前面的例子可以在 C#8 工作。
C# 9 及之后我们不需要使用下划线抛弃局部变量来仅进行类型匹配。
代码如下
decimal flightCost passenger switch
{// C# 9 or later syntaxFirstClassPassenger p p.AirMiles switch{ 35_000 1_500M, 15_000 1_750M,_ 2_000M},BusinessClassPassenger 1_000M,CoachClassPassenger p when p.CarryOnKG 10.0 500M,CoachClassPassenger 650M,_ 800M
};您还可以将关系模式elational pattern与属性模式property pattern结合使用以避免嵌套 switch 表达式如以下代码所示
FirstClassPassenger { AirMiles: 35000 } 1500M,
FirstClassPassenger { AirMiles: 15000 } 1750M,
FirstClassPassenger 2000M,…
使用记录类型
Working with record types
在我们深入研究新的记录语言功能之前让我们先看看 C# 9 及更高版本的其他一些相关新功能。
仅初始化属性
Init-only properties
在本章中您已经使用对象初始化语法来实例化对象并设置初始属性。这些属性也可以在实例化后更改。 有时您希望将属性视为只读字段以便可以在实例化期间而不是实例化之后设置它们。换句话说它们是不可变的。 init 关键字可以实现这一点。它可以用来代替属性定义中的 set 关键字。 由于这是 .NET Standard 2.0 不支持的语言功能。
namespace Packt.Shared;
public class ImmutablePerson
{public string? FirstName { get; init; }public string? LastName { get; init; }
}使用
ImmutablePerson jeff new()
{FirstName Jeff,LastName Winger
};
jeff.FirstName Geoff;//Error!最后一行将会导致编译器报错。 注意如果没有在对象初始值设定项中设置 init-only 属性那也无法在初始化后再去设置它。如果您需要强制设置某个属性请用 required 关键字。 定义 record 类型
init-only 属性为 C# 提供了一些不变性immutability。您可以通过使用 record 类型进一步深化该概念。这些是通过使用 record 关键字而不是或同时 class 关键字来定义的。
这可以使整个对象不可变immutable并且在比较时它就像一个值这将在之后讨论。
不可变记录不应具有在实例化后发生更改的任何状态属性和字段。
相反我们的想法是从现有记录创建新记录。新记录的状态已更改。这称为非破坏性突变non-destructive mutation。为此C# 9 引入了 with 关键字
例子
public record ImmutableVehicle
{public int Wheels { get; init; }public string? Color { get; init; }public string? Brand { get; init; }
}使用
ImmutableVehicle car new()
{Brand Mazda MX-5 RF,Color Soul Red Crystal Metallic,Wheels 4
};
ImmutableVehicle repaintedCar carwith { Color Polymetal Grey Metallic };WriteLine($Original car color was {car.Color}.);
WriteLine($New car color is {repaintedCar.Color}.);运行结果
Original car color was Soul Red Crystal Metallic.
New car color is Polymetal Grey Metallic.即使释放掉 car 的内存repaintedCar 也依然存在。两者相互独立
记录类型的相等性
Equality of record types
记录类型的另一个重要特性是其相等性。具有相同属性值的两条记录被视为相等。这听起来可能并不令人惊讶但如果您使用普通类而不是记录那么它们将不会被认为是相等的。
让我们来看看
namespace Packt.Shared;
public class AnimalClass
{public string? Name { get; set; }
}
public record AnimalRecord
{public string? Name { get; set; }
}使用
AnimalClass ac1 new() { Name Rex };
AnimalClass ac2 new() { Name Rex };
WriteLine($ac1 ac2: {ac1 ac2});AnimalRecord ar1 new() { Name Rex };
AnimalRecord ar2 new() { Name Rex };
WriteLine($ar1 ar2: {ar1 ar2});运行
ac1 ac2: False
ar1 ar2: True… 类实例只有在他们字面上为同一个对象时才相等即它们的内存地址相等。之后还会介绍类型相等的其他内容。 记录中的位置数据成员
Positional data members in records
可以使用位置数据成员positional data members简化定义记录的语法。
有时您可能更愿意提供带有位置参数的构造函数而不是使用带有大括号的对象初始化语法正如您在本章前面所看到的那样。您还可以将其与解构函数结合使用将对象拆分为各个部分如以下代码所示
代码
public record ImmutableAnimal
{public string Name { get; init; }public string Species { get; init; }public ImmutableAnimal(string name, string species){Name name;Species species;}public void Deconstruct(out string name, out string species){name Name;species Species;}
}其实属性、构造器、解构器都可以通过简化语法自动生成
使用简化语法的记录定义如下
// Simpler syntax to define a record that auto-generates the
// properties, constructor, and deconstructor.
public record ImmutableAnimal(string Name, string Species);使用
ImmutableAnimal oscar new(Oscar, Labrador);
var (who, what) oscar; // Calls the Deconstruct method.
WriteLine(${who} is a {what}.);运行
Oscar is a Labrador.…
c# 10 支持创建结构记录struct records将在之后介绍。
… More Information: There are many more ways to use records in your projects. I recom-mend that you review the official documentation at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records. 为类定义主构造函数
Defining a primary constructor for a class
随 C# 12 引入您可以定义一个构造函数作为类定义的一部分。这称为主构造函数。其语法与记录中的位置数据成员相同但行为略有不同。
传统的我们将类定义和构造器定义分开如下所示
public class Headset // Class definition.
{// Constructor.public Headset(string manufacturer, string productName){// You can reference manufacturer and productName parameters in the constructor and the rest of the class.}
}…
使用类主构造函数您可以将两者组合成更简洁的语法如以下代码所示
public class Headset(string manufacturer, string productName);不同于记录的简化定义方法这个主构造函数的参数不会自动变为公有的属性。
我们还需要显式定义这两个属性
public class Headset(string manufacturer, string productName)
{public string Manufacturer { get; set; } manufacturer;public string ProductName { get; set; } productName;
}我们还可以定义默认构造器只需要委托给参数给主构造器
public class Headset(string manufacturer, string productName)
{public string Manufacturer { get; set; } manufacturer;public string ProductName { get; set; } productName;// 默认无参数构造函数调用主构造函数。public Headset() : this(Microsoft, HoloLens) { }
}…
使用
Headset holo new();
WriteLine(${holo.ProductName} is made by {holo.Manufacturer}.);Headset mq new() { Manufacturer Meta, ProductName Quest 3 };
WriteLine(${mq.ProductName} is made by {mq.Manufacturer}.);运行
Vision Pro is made by Apple.
HoloLens is made by Microsoft.
Quest 3 is made by Meta… More Information: You can learn more about primary constructors for classes and structs at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors.