C# 扩展方法、命名参数、可选参数、对象初始化器和匿名类型的初学者教程






4.78/5 (21投票s)
在本文中,我们将讨论 C# 编程语言的四个非常重要的特性。
引言
在本文中,我们将讨论 C# 较新版本中引入的四个新颖且非常重要的特性。来自 C# 2.0 等旧版本的开发者,或者正在学习 C# 的人可能会觉得本文很有用。
背景
C# 是一门发展速度非常快的语言。每个新版本都增加了越来越多的特性。它比 C++ 和 Java 等语言晚出现多年,但在语言特性和开发人员生产力方面设法超越了它们。
在这个简短的教程文章中,我将尝试用我自己的方式来解释 C# 编程语言的四个非常重要的特性。这对于已经了解这些特性的读者可能不太有用,但本文是为那些仍在学习 C# 的初学者准备的。
在本文中,我们将讨论 C# 的以下四个特性。我们还将为每个特性编写一个示例应用程序,以便直观地了解它们。
- 扩展方法
- 命名参数和可选参数
- 对象初始化器
- 匿名类型
Using the Code
扩展方法
开闭原则 (OCP) 指出,我们应该以这样一种方式设计我们的类型,即它们应该是对扩展开放的,但对修改关闭的。C# 中的扩展方法可以被视为一种机制,用于为用户定义的类型甚至原始类型和框架中定义的类型实现 OCP。
扩展方法允许我们通过向现有类型添加额外的方法和功能来扩展现有类型,而无需更改该类型的代码(在大多数情况下我们甚至可能没有该类型的代码)。在扩展方法出现之前,开发人员会创建自己的类型,然后通过继承或在这些类型中包含现有类型来使用它们。这些新类型更像是现有类型的 包装器
,而不是这些类型的实际扩展。
例如,如果我们想获得一个整数的负值的功能,那么我将不得不将我的整数包装在一个自定义类型中,然后使用这个自定义类型来执行操作。
// Old way of extending using wrapper classes
struct MyInt
{
int value;
public MyInt(int val)
{
this.value = val;
}
public int Negate()
{
return -value;
}
}
static void Main(string[] args)
{
// Old way of using wrappers for extensions
MyInt i = new MyInt(53);
Console.WriteLine(i.Negate());
}
现在这种方法确实可行,但问题在于我们并没有真正扩展现有类型。我们创建了一个全新的类型,它包装了现有类型,然后为我们提供了所需的功能。
因此,如果我们真的想扩展一个方法,我们应该能够将这个 Negate
方法直接添加到 int
类型中,并且每个人都应该能够只在 int
类型上调用此方法。无需创建另一个类型来获得此附加功能。扩展方法正是提供了这一点。使用扩展方法,我们可以编写自定义功能,并将它们挂接到现有类型上,以便可以在这些类型上使用它们。
要创建扩展方法,我们需要执行以下步骤:
- 定义一个
static
类。 - 在该类中定义一个
public static
函数,其名称是所需的新方法名称,返回值根据功能而定。 - 传入您想要扩展的类型的参数。这里重要的是在参数前加上
this
关键字,以告知编译器我们需要扩展此类型,并且我们实际上不将此类型作为参数传递。
所以,让我们尝试将这个 Negate
函数添加到 int
类型中。
// Extending using Extension methods
static class MyExtensionMethods
{
public static int Negate(this int value)
{
return -value;
}
}
static void Main(string[] args)
{
//Using extension method
int i2 = 53;
Console.WriteLine(i.Negate());
}
现在,使用扩展方法,让我们在某些现有类型上定义方法。那么,如果我们希望新方法接受一些参数呢?为了做到这一点,我们可以在要扩展的类型参数(与 this
关键字一起使用)之后定义其他参数。让我们在 int
中再定义一个名为 Multiply
的函数,以便看到这一点。
// Extending using Extension methods
static class MyExtensionMethods
{
public static int Negate(this int value)
{
return -value;
}
public static int Multiply(this int value, int multiplier)
{
return value * multiplier;
}
}
static void Main(string[] args)
{
// Passing arguments in extension methods
int i3 = 10;
Console.WriteLine("Passing arguments in extension methods: {0}", i3.Multiply(2));
}
现在,在实现扩展方法之前,需要记住两点:
- 第一点是,扩展方法只能访问类型的
public
属性。 - 扩展方法的签名不应与该类型现有的方法签名相同。
- 只有当包含扩展方法的命名空间在作用域内时,才能使用类型的扩展方法。
- 如果我们定义的扩展方法重载了原始类型现有方法的签名,并且调用出现了歧义,那么重载解析规则将始终选择实例方法而不是扩展方法。
- 如果两个扩展方法之间存在歧义,那么包含更具体参数的方法将被调用。
如果我们牢记以下几点,我们就可以真正利用扩展方法来更好地设计和扩展类型,并提供更好的类型抽象。LINQ
大量使用扩展方法。强烈建议查看 LINQ
如何与 LAMBDA
表达式结合使用扩展方法。
注意:有关 Lambda 表达式的信息可以在这里找到:C# 委托、匿名函数和 Lambda 表达式基础知识的初学者教程[^]。
可选参数
现在,在我们继续讨论命名参数和位置参数之前,让我们先谈谈可选参数。在旧版本的 C# 中,不可能为函数创建可选参数,也就是说,如果我需要为函数中的参数设置默认值,唯一的办法就是函数重载。
假设我们要定义一个 Multiply
函数,调用者可以传递两个数字并获得结果。但是,如果用户只想传递一个参数,这也是允许的,函数会将这个数字乘以 1 并返回值。现在,在旧版本的 C# 中,这是通过函数重载完成的,如下所示:
private static int Multiply(int num1, int num2)
{
return num1 * num2;
}
private static int Multiply(int num1)
{
return Multiply(num1, 1);
}
static void Main(string[] args)
{
// Testing OldWay of default parameters
Console.WriteLine(Multiply(2));
}
现在这提供了所需的结果,但问题在于,随着函数参数数量的增加,重载函数的数量也会随之增加,以提供所有可能的组合。拥有大量重载函数版本会给维护带来噩梦。
现在,为了解决这个问题,C# 引入了在函数中提供可选参数的机制。所以,使用可选参数,上面的函数将如下所示:
private static int Multiply2(int num1, int num2 = 1)
{
return num1 * num2;
}
static void Main(string[] args)
{
// Testing default parameters
Console.WriteLine(Multiply2(2));
}
拥有可选参数的唯一限制是,默认参数应该始终放在函数所有必需参数之后。
命名参数
现在,拥有可选参数的可能性非常棒,但有一个小问题,如果函数中有多个可选参数。让我们看一个这样的函数:
private void TestFunction(int one, int two, int three = 3, int four = 4, int five = 5)
{
// Some implementation here
}
现在,我们想传递 four
和 five
的值,但想使用 three
的默认值。在正常情况下这是不可能的。为了执行此操作,我们需要使用命名参数。命名参数使我们有可能使用其名称传递特定的函数参数。所以,让我们尝试调用这个函数。
TestFunction(one: 11, two: 22, four: 44, five: 55);
现在像上面这样调用函数,我们使用了 three
的默认值,同时为 four
和 five
传递了值,它们是定义在 three
之后的可选参数。
我们还为变量 one
和 two
使用了命名参数,但由于我们按相应参数位置传递 one
和 two
,因此可以省略它们的名称,因此它们将仅作为位置参数传递,而 four
和 five
作为命名参数。
TestFunction(11, 22, four: 44, five: 55);
对象初始化器
C# 中的对象初始化器为我们提供了一种简单的方法,可以在构造对象时初始化对象的所有属性。假设我们有一个简单的类,其中包含一些 public
属性,如下所示:
// A simple class to test object initializers
class Book
{
public int BookID { get; set; }
public string BookName { get; set; }
public string AuthorName { get; set; }
public string ISBN { get; set; }
}
现在,让我们使用对象初始化器创建这个类的对象,如下所示:
static void Main(string[] args)
{
// using object initializers
Book book = new Book
{
BookID = 1,
BookName = "MVC Music Store Tutorial",
AuthorName = "Jon Galloway",
ISBN = "NA"
};
}
关于自动属性的说明
现在,我定义类属性的方式可能对于使用 C# 2.0 或更早版本的用户来说是新的。这些属性被定义为自动属性。如果我们的类的任何变量仅用作数据存储,并且在设置或获取它之前没有任何验证逻辑,那么我们可以完全跳过创建变量并直接创建属性。编译器将在运行时负责为该属性生成一个变量。
隐式类型变量和匿名类型
在 C# 3.0 及更高版本中,我们可以定义一个变量而不指定其实际类型,前提是该变量正在被赋值。这些变量是隐式类型的,也就是说,变量的类型是分配给它的变量的类型。
var var1 = new Dictionary<string, Book>();
var var2 = "some random string";
var var3 = Multiply(3, 4);
现在 var1
的类型是 Dictionary<string, Book>
,因为它就是这样创建的。var2
的类型是 string
,因为分配给它的是一个 string
,而 var3
的类型是 int
,因为函数返回的是 int
。
var
并不定义一个可以赋任何类型的动态变量。它只是一个语法糖,这样我们就可以在局部作用域中跳过定义变量的类型。
var
只是一个语法糖。尽管如此,还有另一件重要的事情。我们应该尽量不要使用 var
关键字来声明局部变量,因为对于这些变量,我们可以定义显式类型。原因在于它降低了代码的可读性。那么问题来了,什么时候应该使用 var
呢?
我们应该只在必须时才使用 var
。唯一需要使用 var
的地方是与匿名类型相关,即在运行时即时创建且其类型名称在编译时不可用的类型。LINQ 查询经常需要创建匿名类型,这是我们应该使用隐式类型变量,即 var
的唯一地方。
让我们创建一个简单的匿名类型,并使用隐式类型变量对其进行赋值和使用。
// Mandatory use of var, i.e., anonymous types
var aVar = new { Name = "Rahul", Age = 30 };
Console.WriteLine("{0} is {1} years of age", aVar.Name, aVar.Age);
注意:匿名类型强制要求使用 var
关键字。对于所有其他场景,由于上述可读性降低的问题,不建议使用 var
。如果使用了 LINQ
投影,那么使用 var
关键字是绝对必要的。
看点
在本文中,我们讨论了 C# 3.0 中引入的一些特性。本文是为完全不了解这些 C# 特性的初学者准备的,并且完全是从初学者的角度编写的。我强烈建议您查看代码以全面了解所有这些特性。希望本文内容丰富。
历史
- 2013年4月5日:第一个版本