百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

C# 13 和 .NET 9 全知道 :5 构建您自己的类型——面向对象编程 (4)

yuyutoo 2025-01-27 01:05 4 浏览 0 评论

理解 ref 返回

在 C# 7 或更高版本中, ref 关键字不仅用于将参数传递给方法;它还可以应用于 return 值。这允许外部变量引用内部变量并在方法调用后修改其值。这在高级场景中可能很有用,例如将占位符传递到大数据结构中,但这超出了本书的范围。如果您想了解更多信息,可以阅读以下链接中的内容:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref#reference-return-values。

现在,让我们回到查看返回值的方法的更高级场景。

结合多个返回值使用元组

每种方法只能返回一个单一类型的值。该类型可以是简单类型,如前例中的 string ;复杂类型,如 Person ;或集合类型,如 List<Person>

想象一下,我们想要定义一个名为 GetTheData 的方法,它需要返回一个 string 值和一个 int 值。我们可以定义一个新的类名为 TextAndNumber ,它包含一个 string 字段和一个 int 字段,并返回该复杂类型的实例,如下面的代码所示:

public class TextAndNumber
{
  public string Text;
  public int Number;
}
public class LifeTheUniverseAndEverything
{
  public TextAndNumber GetTheData()
  {
    return new TextAndNumber
    {
      Text = "What's the meaning of life?",
      Number = 42
    };
  }
}

但仅为了组合两个值而定义一个类是不必要的,因为在现代版本的 C#中,我们可以使用元组。元组是一种将两个或更多值组合成一个单元的高效方式。我读作 tuh-ples,但我也听到其他开发者读作 too-ples。番茄,西红柿,土豆,马铃薯,我想。

元组在 F#等一些语言中自第一版起就是一部分,但.NET 直到 2010 年的.NET 4 才添加了对它们的支持,使用 System.Tuple 类型。

2017 年,随着 C# 7 的推出,C#添加了对元组的语言语法支持,使用括号字符 () ,同时.NET 也添加了一个新的 System.ValueTuple 类型,在某些常见场景下比旧的.NET 4 System.Tuple 类型更高效。C#的元组语法使用更高效的那个。

让我们探索元组:

  1. Person.cs 中,添加语句定义一个返回结合 stringint 的元组的方法,如下所示代码:
// Method that returns a tuple: (string, int).
public (string, int) GetFruit()
{
  return ("Apples", 5);
}

Program.cs 中添加调用 GetFruit 方法的语句,然后输出元组的字段,这些字段自动命名为 Item1Item2 ,如下所示:

(string, int) fruit = bob.GetFruit();
WriteLine(#34;{fruit.Item1}, {fruit.Item2} there are.");
  1. 运行 PeopleApp 项目并查看结果,如下所示输出:
Apples, 5 there are.

为元组字段命名

要访问元组的字段,默认名称为 Item1Item2 等等。

您可以显式指定字段名称:

  1. Person.cs 中,添加语句定义一个返回具有命名字段的元组的函数,如下所示代码所示:
// Method that returns a tuple with named fields.
public (string Name, int Number) GetNamedFruit()
{
  return (Name: "Apples", Number: 5);
}

Program.cs 中,添加调用方法并输出元组的命名字段的语句,如下所示代码

var fruitNamed = bob.GetNamedFruit();
WriteLine(#34;There are {fruitNamed.Number} {fruitNamed.Name}.");

我们使用 var 来缩短以下完整语法:

(string Name, int Number) fruitNamed = bob.GetNamedFruit();

  1. 运行 PeopleApp 项目并查看结果,如下所示输出:
There are 5 Apples.

如果您从另一个对象构造元组,可以使用 C# 7.1 中引入的元组命名推断功能。

  1. Program.cs 中,创建两个元组,每个元组由一个 string 和一个 int 值组成,如下代码所示:
var thing1 = ("Neville", 4);
WriteLine(#34;{thing1.Item1} has {thing1.Item2} children.");
var thing2 = (bob.Name, bob.Children.Count);
WriteLine(#34;{thing2.Name} has {thing2.Count} children.");

在 C# 7 中,两者都会使用 Item1Item2 命名方案。在 C# 7.1 及以后版本中, thing2 可以推断出 NameCount 的名称。

别名元组

C# 12 中引入了为元组别名的功能,以便您可以为类型命名并在声明变量和参数时使用该类型名,例如以下代码所示:

using UnnamedParameters = (string, int); // Aliasing a tuple type.
// Aliasing a tuple type with parameter names.
using Fruit = (string Name, int Number);

当别名元组时,使用标题大小写命名约定为其参数,例如, NameNumberBirthDate

让我们看看一个例子:

  1. Program.cs 中,在文件顶部定义一个命名元组类型,如下所示:
using Fruit = (string Name, int Number); // Aliasing a tuple type.

Program.cs 中,复制并粘贴调用 GetNamedFruit 方法的语句,并将 var 更改为 Fruit ,如下所示:

// Without an aliased tuple type.
//var fruitNamed = bob.GetNamedFruit();
// With an aliased tuple type.
Fruit fruitNamed = bob.GetNamedFruit();
  1. 运行 PeopleApp 项目并注意结果相同。

解构元组

您还可以将元组解构为单独的变量。解构声明与命名字段元组的语法相同,但元组没有命名变量,如下代码所示:

// Store return value in a tuple variable with two named fields.
(string name, int number) namedFields = bob.GetNamedFruit();
// You can then access the named fields.
WriteLine(#34;{namedFields.name}, {namedFields.number}");
// Deconstruct the return value into two separate variables.
(string name, int number) = bob.GetNamedFruit();
// You can then access the separate variables.
WriteLine(#34;{name}, {number}");

解构会将元组拆分为其部分,并将这些部分分配给新的变量。让我们看看它是如何运作的:

  1. Program.cs 中,添加语句以解构 GetFruit 方法返回的元组,如下所示代码:
(string fruitName, int fruitNumber) = bob.GetFruit();
WriteLine(#34;Deconstructed tuple: {fruitName}, {fruitNumber}");

运行 PeopleApp 项目并查看结果,如下所示输出:

Deconstructed tuple: Apples, 5

解构其他类型的元组

元组不是唯一可以解构的类型。任何类型都可以有特殊的方法,命名为 Deconstruct ,将对象分解成部分。只要它们有不同的签名,你可以有任意多的 Deconstruct 方法。让我们为 Person 类实现一些:

  1. Person.cs 中,添加两个具有 out 参数定义的 Deconstruct 方法,用于我们想要分解的部分,如下所示代码:
// 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;
}

Program.cs 中,添加语句以解构 bob ,如下所示:

var (name1, dob1) = bob; // Implicitly calls the Deconstruct method.
WriteLine(#34;Deconstructed person: {name1}, {dob1}");
var (name2, dob2, fav2) = bob;
WriteLine(#34;Deconstructed person: {name2}, {dob2}, {fav2}");

您没有显式调用 Deconstruct 方法。当您将对象赋值给元组变量时,该方法会隐式调用。

  1. 运行 PeopleApp 项目并查看结果,如下所示输出:
Deconstructed person: Bob Smith, 12/22/1965 4:28:00 PM -05:00
Deconstructed person: Bob Smith, 12/22/1965 4:28:00 PM -05:00,
StatueOfZeusAtOlympia

实现使用本地函数的功能

C# 7 引入的一项语言特性是能够定义局部函数。

本地函数是局部变量的方法等价物。换句话说,它们是只能在定义它们的包含方法内部访问的方法。在其他语言中,它们有时被称为嵌套函数或内部函数。

本地函数可以在方法内部任何位置定义:顶部、底部,甚至中间的某个位置!

我们将使用本地函数来实现阶乘计算:

  1. Person.cs 中,添加语句定义一个 Factorial 函数,该函数在其内部使用局部函数来计算结果,如下面的代码所示:
// Method with a local function.
public static int Factorial(int number)
{
  if (number < 0)
  {
    throw new ArgumentException(
      #34;{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);
  }
}

Program.cs 中添加调用 Factorial 函数的语句,并将返回值写入控制台,同时进行异常处理,如下所示:

// Change to -1 to make the exception handling code execute.
int number = 5;
try
{
  WriteLine(#34;{number}! is {Person.Factorial(number)}");
}
catch (Exception ex)
{
  WriteLine(#34;{ex.GetType()} says: {ex.Message} number was {number}.");
}

运行 PeopleApp 项目并查看结果,如下所示输出:

5! is 120
  1. 修改数字为 -1 ,以便我们可以检查异常处理。
  2. 运行 PeopleApp 项目并查看结果,如下所示输出:
System.ArgumentException says: number cannot be less than zero. number was -1.

使用部分拆分类

当与多个团队成员合作进行大型项目,或与特别大且复杂的类实现合作时,能够将类的定义分散到多个文件中是有用的。您可以使用 partial 关键字来完成此操作。

想象一下,我们想要向 Person 类添加由工具自动生成的语句,例如从数据库读取模式信息的对象关系映射器。如果类定义为 partial ,那么我们可以将类拆分为自动生成的代码文件和手动编辑的代码文件。

让我们编写一些代码来模拟这个示例:

  1. Person.cs 中添加 partial 关键字,如以下代码所示:
public partial class Person
  1. PacktLibraryNet2 项目/文件夹中,添加一个名为 PersonAutoGen.cs 的新类文件。
  2. 在新的文件中添加如下代码所示的语句:
namespace Packt.Shared;
// This file simulates an auto-generated class.
public partial class Person
{
}
  1. 构建 PacktLibraryNet2 项目。如果您看到 CS0260 Missing partial modifier on declaration of type 'Person'; another partial declaration of this type exists 错误,请确保您已将 partial 关键字应用于 Person 两个类。

此章节中我们编写的其余代码将保存在 PersonAutoGen.cs 文件中。

部分方法

在 2007 年的.NET Framework 3 中引入了部分方法。它们是一种允许在类的一部分中定义方法签名的功能,而实际实现则提供在另一部分。部分方法在代码生成和手动代码共存的情况下特别有用,例如在由 Entity Framework Core 或源代码生成器等工具生成的代码中。

以下列出了 partial 方法的一些关键特性:

  • 部分方法使用 partial 关键字声明。声明提供方法签名,而实现提供方法体。
  • 实现部分方法是非强制的。如果声明了部分方法但没有实现,编译器将移除对该方法的调用,不会抛出错误。
  • 部分方法默认为私有且不能有访问修饰符。它们还必须返回 void ,并且不能有 out 参数。
  • 部分方法不能 virtual

部分方法常用于涉及代码生成的场景中,其中提供了一个基本结构,并且可以添加自定义逻辑而不修改生成的代码。

想象一下,你有一个类文件,如下所示:

// MyClass1.cs
public partial class MyClass
{
  // No method body in the declaration.
  partial void OnSomethingHappened();
  public void DoWork()
  {
    // Some work here.
    // Call the partial method.
    OnSomethingHappened();
  }
}

现在,想象一下你还有一个类文件,如下代码所示:

// MyClass2.cs
public partial class MyClass
{
  partial void OnSomethingHappened()
  {
    Console.WriteLine("Something happened.");
  }
}

在前面示例中, OnSomethingHappened 是在 MyClass1.cs 中声明的部分方法,并在 MyClass2.cs 中实现。方法 DoWork 调用部分方法,如果提供了实现,则打印一条消息。

如果示例中声明了 OnSomethingHappened 但没有实现,那么 C#编译器将移除对 DoWork 中的 OnSomethingHappened 的调用,并且不会抛出错误。

部分方法通常用于自动生成的代码中,开发者可以在不修改生成代码的情况下挂钩到该过程。如果 MyClass1.cs 文件是自动生成的,情况就会是这样。

C#中的部分方法提供了一种强大的方式来扩展和自定义生成的代码,而不直接修改它。它们提供了一种干净的机制来插入自定义行为,确保代码生成和自定义逻辑可以共存。通过利用部分方法,开发者可以保持生成代码和自定义代码之间的清晰分离,提高可维护性和可读性。现在你已经看到了许多字段和方法的示例,我们将探讨一些专门的方法类型,这些类型可以用来访问字段,以提供控制和改善开发者的体验。

控制访问属性和索引器

之前,您创建了一个名为 GetOrigin 的方法,该方法返回一个包含人名和出处的 string 。像 Java 这样的语言经常这样做。C#有更好的方法,这被称为属性。

属性简单来说就是一个方法(或一对方法),当您想要获取或设置值时,它表现得像一个字段,但它在行为上像一个方法,从而简化了语法,并在设置和获取值时实现了功能,如验证和计算。

一个字段和属性之间的基本区别在于,字段为数据提供了一个内存地址。您可以传递这个内存地址给外部组件,比如 Windows API C 风格函数调用,然后它可以修改数据。属性不提供其数据的内存地址,这提供了更多的控制。您所能做的就是请求属性获取或设置数据。然后属性执行语句并可以决定如何响应,包括拒绝请求!

定义只读属性

一个 readonly 属性只有一个 get 实现:

  1. PersonAutoGen.cs 中,在 Person 类中,添加语句以定义三个属性:第一个属性将执行与 GetOrigin 方法相同的功能,使用与所有版本的 C#兼容的属性语法。第二个属性将返回一条问候消息,使用 C# 6 及以后版本的 lambda 表达式体 => 语法。第三属性将计算个人的年龄。

以下是代码:

#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);
  }
}
// Two readonly properties defined using C# 6 or later
// lambda expression body syntax.
public string Greeting => #34;{Name} says 'Hello!'";
public int Age => DateTime.Today.Year - Born.Year;
#endregion

好的做法:这不是计算某人年龄的最佳方法,但我们不是在学习如何从出生日期和时间计算年龄。如果您需要正确地这样做,请阅读以下链接的讨论:https://stackoverflow.com/questions/9/how-do-i-calculate-someones-age-in-c。

  1. Program.cs 中,添加获取属性的语句,如下所示代码:
Person sam = new()
{
  Name = "Sam",
  Born = new(1969, 6, 25, 0, 0, 0, TimeSpan.Zero)
};
WriteLine(sam.Origin);
WriteLine(sam.Greeting);
WriteLine(sam.Age);

运行 PeopleApp 项目并查看结果,如下所示输出:

Sam was born on Earth
Sam says 'Hello!'
54

输出显示 54 ,因为我是在 Sam 54 岁时,即 2023 年 7 月 5 日运行的控制台应用程序。

定义可设置属性

要创建一个可设置的属性,您必须使用较旧的语法并提供一对方法——不仅是一个 get 部分,还包括一个 set 部分:

  1. PersonAutoGen.cs 中,添加语句以定义一个具有 stringset 方法(也称为 getter 和 setter)的 get 属性,如下所示代码:
// A read-write property defined using C# 3 auto-syntax.
public string? FavoriteIceCream { get; set; }

尽管您没有手动创建一个字段来存储该人的最爱冰淇淋,但它已经存在,由编译器自动为您创建。

有时,您需要更多控制来设置属性时的行为。在这种情况下,您必须使用更详细的语法,并手动创建一个 private 字段来存储属性的值。

  1. PersonAutoGen.cs 中,添加语句以定义一个名为 private string 的字段,称为后置字段,如下面的代码所示:
// A private backing field to store the property value.
private string? _favoritePrimaryColor;

良好的实践:尽管没有正式的标准来命名私有字段,但最常见的是使用带下划线前缀的驼峰命名法。

  1. PersonAutoGen.cs 中,添加语句以定义一个具有 getsetstring 属性,并在 setter 中添加验证逻辑,如下所示代码:
// 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(
          #34;{value} is not a primary color. " +
          "Choose from: red, green, blue.");
    }
  }
}

好的实践:避免在 getter 和 setter 中添加过多的代码。这可能表明你的设计存在问题。考虑添加私有方法,然后在 setget 方法中调用这些方法以简化你的实现。

  1. Program.cs 中,添加设置 Sam 最喜欢的冰淇淋和颜色的语句,然后将其写入,如下所示代码:
sam.FavoriteIceCream = "Chocolate Fudge";
WriteLine(#34;Sam's favorite ice-cream flavor is {sam.FavoriteIceCream}.");
string color = "Red";
try
{
  sam.FavoritePrimaryColor = color;
  WriteLine(#34;Sam's favorite primary color is {sam.FavoritePrimaryColor}.");
}
catch (Exception ex)
{
  WriteLine("Tried to set {0} to '{1}': {2}",
    nameof(sam.FavoritePrimaryColor), color, ex.Message);
}

印刷版书籍限制在约 820 页。如果我在所有代码示例中添加异常处理代码,就像我们在这里所做的那样,那么我可能不得不至少从书中删除一章来腾出足够的空间。在未来,我不会明确要求你添加异常处理代码,但我会养成在需要时自己添加的习惯。

  1. 运行 PeopleApp 项目并查看结果,如下所示输出:
Sam's favorite ice-cream flavor is Chocolate Fudge.
Sam's favorite primary color is Red.
  1. 尝试将颜色设置为红色、绿色或蓝色以外的任何值,如黑色。
  2. 运行 PeopleApp 项目并查看结果,如下所示输出:
Tried to set FavoritePrimaryColor to 'Black': Black is not a primary color. Choose from: red, green, blue.

良好实践:当您想在读取或写入字段时执行语句而不使用方法对(如 GetAgeSetAge )时,请使用属性而不是字段。

相关推荐

Mysql和Oracle实现序列自增(oracle创建序列的sql)

Mysql和Oracle实现序列自增/*ORACLE设置自增序列oracle本身不支持如mysql的AUTO_INCREMENT自增方式,我们可以用序列加触发器的形式实现,假如有一个表T_WORKM...

关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)

概述今天主要简单介绍一下Oracle12c的一些新特性,仅供参考。参考:http://docs.oracle.com/database/121/NEWFT/chapter12102.htm#NEWFT...

MySQL CREATE TABLE 简单设计模板交流

推荐用MySQL8.0(2018/4/19发布,开发者说同比5.7快2倍)或同类型以上版本....

mysql学习9:创建数据库(mysql5.5创建数据库)

前言:我也是在学习过程中,不对的地方请谅解showdatabases;#查看数据库表createdatabasename...

MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别

执行"CREATETABLE新表ASSELECT*FROM原表;"后,新表与原表的字段一致,但主键、索引不会复制到新表,会把原表的表记录复制到新表。...

Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出

在街上看到的PandaDunk的超载可能让一些球鞋迷们望而却步,但Dunk的浪潮仍然强劲,看不到尽头。我们看到的很多版本都是为女性和儿童制作的,这种新配色为后者引入了一种令人耳目一新的新选择,而...

美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍

多功能雷达AN/SPY-1的特性和技术能力,该雷达已经在美国海军服役了30多年,其修改-AN/SPY-1A、AN/SPY-1B(V)、AN/SPY-1D、AN/SPY-1D(V),以及雷神...

汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)

全面分析汽车音响使用或安装技术常识一:主机是大多数人最熟习的音响器材,有关主机的各种性能及规格,也是耳熟能详的事,以下是一些在使用或安装时,比较需要注意的事项:LOUDNESS:几年前的主机,此按...

【推荐】ProAc Response系列扬声器逐个看

有考牌(公认好声音)扬声器之称ProAcTablette小音箱,相信不少音响发烧友都曾经,或者现在依然持有,正当大家逐渐掌握Tablette的摆位设定与器材配搭之后,下一步就会考虑升级至表现更全...

#本站首晒# 漂洋过海来看你 — BLACK&amp;DECKER 百得 BDH2000L无绳吸尘器 开箱

作者:初吻给了烟sco混迹张大妈时日不短了,手没少剁。家里有了汪星人,吸尘器使用频率相当高,偶尔零星打扫用卧式的实在麻烦(汪星人:你这分明是找借口,我掉毛是满屋子都有,铲屎君都是用卧式满屋子吸的,你...

专题|一个品牌一件产品(英国篇)之Quested(罗杰之声)

Quested(罗杰之声)代表产品:Q212FS品牌介绍Quested(罗杰之声)是录音监听领域的传奇品牌,由英国录音师RogerQuested于1985年创立。在成立Quested之前,Roger...

常用半导体中英对照表(建议收藏)(半导体英文术语)

作为一个源自国外的技术,半导体产业涉及许多英文术语。加之从业者很多都有海外经历或习惯于用英文表达相关技术和工艺节点,这就导致许多英文术语翻译成中文后,仍有不少人照应不上或不知如何翻译。为此,我们整理了...

Fyne Audio F502SP 2.5音路低音反射式落地音箱评测

FyneAudio的F500系列,有新成员了!不过,新成员不是新的款式,却是根据原有款式提出特别版。特别版产品在原有型号后标注了SP字样,意思是SpecialProduction。Fyne一共推出...

有哪些免费的内存数据库(In-Memory Database)

以下是一些常见的免费的内存数据库:1.Redis:Redis是一个开源的内存数据库,它支持多种数据结构,如字符串、哈希表、列表、集合和有序集合。Redis提供了快速的读写操作,并且支持持久化数据到磁...

RazorSQL Mac版(SQL数据库查询工具)

RazorSQLMac特别版是一款看似简单实则功能非常出色的SQL数据库查询、编辑、浏览和管理工具。RazorSQLformac特别版可以帮你管理多个数据库,支持主流的30多种数据库,包括Ca...

取消回复欢迎 发表评论: