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

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

yuyutoo 2025-01-27 01:05 1 浏览 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 )时,请使用属性而不是字段。

相关推荐

java高级用法之:绑定CPU的线程Thread-Affinity

简介在现代计算机系统中,可以有多个CPU,每个CPU又可以有多核。为了充分利用现代CPU的功能,JAVA中引入了多线程,不同的线程可以同时在不同CPU或者不同CPU核中运行。但是对于JAVA程序猿来说...

迁移至最新版?Oracle结束Java7生命周期

据国外报道,Oracle于2015年4月停止发布Java7安全补丁和升级包,促使用户迁移至Java8或购买Java7的长期商业支持服务。未来或可能有其它第三方机构为其提供公共更新。(图片来源ho...

配置Java环境变量:(WIN7为例)(配置java 环境变量)

1.JAVA_HOME变量的设置2.Path变量的设置3.ClassPath变量的设置...

Java中的CPU占用高和内存占用高的问题排查

作者|归去来兮辞...

Java 8:一文掌握 Lambda 表达式 | CSDN 博文精选

作者|Android大强哥责编|郭芮出品|CSDN博客本文将介绍Java8新增的Lambda表达式,包括Lambda表达式的常见用法以及方法引用的用法,并对Lambda...

Mongodb centos7安装(mongodb4.4安装)

下载官方下载地址:MongoDBCommunityDownloads下载并解压...

Java常用的7大排序算法汇总(java排序总结)

这段时间闲了下来,就抽了点时间总结了下java中常用的七大排序算法,希望以后可以回顾!1.插入排序算法插入排序的基本思想是在遍历数组的过程中,假设在序号i之前的元素即[0..i-1]都已经排好...

Linux新手入门系列:Linux下jdk安装配置

本系列文章是把作者刚接触和学习Linux时候的实操记录分享出来,内容主要包括Linux入门的一些理论概念知识、Web程序、mysql数据库的简单安装部署,希望能够帮到一些初学者,少走一些弯路。...

PowerDesigner在64位JDK填坑记.md

系统环境利用powerdesigner反向生成表结构时报:**connectiontestfailed**胖先生使用的JDBC方式连接,无法连接到MySQL,前段时间我选择了逃避操作系统:...

2015年新春win7系统64位装机驱动加强版下载

软件格式:NTFS软件语言:简体中文软件大小:4.21G适用环境:适合于台式电脑-笔记本电脑!MD5校检:5349DAA96D86FF9AE06BC4D6C0438C5F系统特色:釆用最齐全的驱动,优...

win7旗舰版64位系统还原网络设置方法

有一位深度技术...

我们必须要了解的Java位运算(不仅限于Java)

我们必须要了解的Java位运算(不仅限于Java)-陈咬金-博客园...

二进制数值数据的运算方法(二进制的数值计算)

补码加法与减法的运算规则加减法运算是计算机中最基本的运算,通常选用补码实现,实现的算法是:...

西门子S7-300PLC的数据类型介绍(西门子300plc数据类型转换)

之前我们说了PLC的数据类型分为三类,基本数据类型、复杂数据类型、参数类型数数据类型。我们只介绍了三类:位、字节、字、双字。下面我们再介绍另外几种:整数(INT)、双精度整数(DINT),它们是有符号...

源码,反码,补码其实真的很简单(源码 补码反码)

1、计算机在任何情况下都只能识别二进制2、计算机在底层存储数据的时候,一律存储的是“二进制的补码形式”,计算机采用补码形式存储数据的原因是:补码形式效率最高。3、记住:...

取消回复欢迎 发表评论: