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

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

bigegpt 2025-01-05 15:49 33 浏览

理解 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 )时,请使用属性而不是字段。

相关推荐

ActiveAndroid使用(对象化数据库)

配置模块的build.gradlerepositories{mavenCentral()mavenLocal()maven{url"https://oss.sonatype.org/conte...

AndroidStudio下的依赖管理(android app依赖外部jar包)

在开发中用第三方库是很常见的事,如何在AndroidStudio下管理这些依赖呢?这就是这篇文章的目的。目录Maven/Ivy仓库依赖Module依赖aar文件依赖jar文件依赖例子完整代码一、Mav...

Android Studio之gradle的配置与介绍

1、gradle的简单介绍Gradle是可以用于Android开发的新一代的BuildSystem,也是AndroidStudio默认的build工具。其实Gradle脚本是基于一种JVM语言—...

Android中的run-as命令带来的安全问题

一、前言最近一周比较忙,没时间写东西了,今天继续开始我们今天的话题:run-as命令,在上周的开发中,遇到一个问题,就是在使用run-as命令的时候出现了一个错误,不过当时因为工作进度的问题,这问题就...

Android系统级深入开发——input驱动程序

1、Input驱动程序是Linux输入设备的驱动程序,分成游戏杆(joystick)、鼠标(mouse和mice)和事件设备(Eventqueue)3种驱动程序。其中事件驱动程序是目前通用的驱动程序...

Android项目中如何用好构建神器Gradle?

CSDN移动将持续为您优选移动开发的精华内容,共同探讨移动开发的技术热点话题,涵盖移动应用、开发工具、移动游戏及引擎、智能硬件、物联网等方方面面。如果您想投稿、参与内容翻译工作,或寻求近匠报道,请发送...

Android Studio自定义文件类头(android studio自定义标题栏)

--简书作者谢恩铭转载请注明出处今天给大家介绍一个很简单的"小"技巧。平时,我们在AndroidStudio中开发Android时,总免不了要创建新的文件,也许是Java文件,也许是C...

C语言#include头文件真的是插入代码吗?

若文章对您有帮助,欢迎关注程序员小迷。助您在编程路上越走越好!编译器理论和实作既是又不是。从编译器理论理解,#include头文件"相当于"插入了头文件的代码,以供源代码引用(宏定...

Android 系统核心机制binder(03)binder C++层实现

本章关键点总结&说明:这里主要关注BinderC++部分即可,看到,也是本章节的核心内容,主要就是以C++封装的框架为主来解读binder。之前主要针对于底层驱动binder的数据交互以及...

Java对象序列化与反序列化的那些事

Java对象序列化与反序列化的那些事在Java的世界里,对象序列化和反序列化就像一对孪生兄弟,它们共同构成了Java对象存储和传输的基础。如果你曾经尝试将对象保存到文件中,或者在网络中传输对象,那么你...

Java对象序列化剖析(java 对象序列化)

对象序列化的目的1)希望将Java对象持久化在文件中2)将Java对象用于网络传输实现方式如果希望一个类的对象可以被序列化/反序列化,那该类必须实现java.io.Serializable接口或jav...

C++模板 - 16(SFINAE)(c++模板编程)

C++支持函数重载,同一个函数名,只要它的签名不一样,可以声明若干个版本(这个特性也是必须的,不然构造函数就只能有一个了)。现在函数的重载集合中又加入了新的成员-函数模板,事情就变得越发有趣起来,...

NewtoSoft.Json相关使用技巧(newtosoft.json相关使用技巧有哪些)

  本篇将为大家介绍Newtonsoft.Json的一些高级用法,可以修改很少的代码解决上述问题。Newtonsoft.Json介绍  在做开发的时候,很多数据交换都是以json格式传输的。而使用Js...

C#调用DeepSeek API(c#调用deepseek api 流式输出)

一、官方网站二、DeepSeek测试DeepSeek三大适用模式:基础模型(V3)、深度思考(R1)、联网搜索。基础模型(V3)深度思考(R1)联网搜索三、C#调用DeepSeekAPI核心代码//...

.NET性能系列文章二:Newtonsoft.Json vs System.Text.Json

微软终于追上了?图片来自GlennCarstens-Peters[1]Unsplash[2]欢迎来到.NET性能系列的另一章。这个系列的特点是对.NET世界中许多不同的主题进行研究、基准和比较...