扩展方法(C# 编程指南)
扩展方法(C# 编程指南)
谢谢。
本文内容
扩展方法使您能够将方法“添加”到现有类型,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。扩展方法是静态方法,但可以像扩展类型上的实例方法一样调用。对于用 C#、F# 和 Basic 编写的客户端代码,调用扩展方法与调用类型中定义的方法没有区别。
最常见的扩展方法是 LINQ 标准查询运算符,它将查询功能添加到现有的 .. 和 ... 类型。要使用标准查询运算符,首先使用 using .Linq 指令将它们置于范围内。然后方法,任何实现的类型似乎都具有 、 、 等的实例方法。当您在 List 或 Array 等类型的实例之后键入“点”时,可以在语句完成中看到这些附加方法。
例子
以下示例显示了如何对整数数组调用标准查询运算符方法。括号内的表达式是表达式。许多标准查询运算符将表达式作为参数,但这不是扩展方法的要求。有关详细信息,请参阅表达式。
class ExtensionMethods2
{
static void Main()
{
int[] ints = { 10, 45, 15, 39, 21, 26 };
var result = ints.OrderBy(g => g);
foreach (var i in result)
{
System.Console.Write(i + " ");
}
}
}
//Output: 10 15 21 26 39 45
扩展方法被定义为静态方法,但它们是通过实例方法语法调用的。它们的第一个参数指定方法操作的类型。参数前面有这个修饰符。仅当您使用 using 指令将命名空间显式导入源代码时,扩展方法才在范围内。
以下示例演示了为 .班级。它是在一个非嵌套的、非泛型的静态类中定义的:
namespace ExtensionMethods
{
public static class MyExtensions
{
public static int WordCount(this string str)
{
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
使用此指令可以将 using 扩展方法置于范围内:
using ExtensionMethods;
此外,可以使用以下语法从应用程序调用扩展方法:
string s = "Hello Extension Methods";
int i = s.WordCount();
在代码中,可以使用实例方法语法调用扩展方法。编译器生成的中间语言 (IL) 将代码转换为对静态方法的调用。并没有真正违反封装原则。扩展方法不能访问它们扩展类型的私有变量。
类和方法都可以像所有其他成员一样访问。方法可以像其他方法一样被调用,如下:
string s = "Hello Extension Methods";
int i = MyExtensions.WordCount(s);
以上C#代码:
有关详细信息,请参阅如何实现和调用自定义扩展方法。
一般来说,您更有可能调用扩展方法而不是实现自己的方法。因为扩展方法是使用实例方法语法调用的,所以它们不需要任何特殊知识就可以从客户端代码中使用它们。要为特定类型启用扩展方法,只需将 using 指令添加到定义这些方法的命名空间。例如,要使用标准查询运算符,请将此 using 指令添加到您的代码中:
using System.Linq;
(您可能还必须添加对 .Core.dll 的引用。)您会注意到标准查询运算符现在作为可用于大多数类型的附加方法出现。
在编译时绑定扩展方法
扩展方法可用于扩展类或接口,但扩展方法不能被覆盖。永远不会调用与接口或类方法具有相同名称和签名的扩展方法。在编译时,扩展方法的优先级总是低于类型本身定义的实例方法。换句话说,如果一个类型有一个名为 (int i) 的方法并且你有一个具有相同签名的扩展方法,编译器总是绑定到那个实例方法。当编译器遇到方法调用时,它首先在该类型的实例方法中寻找匹配的方法。如果没有找到匹配的方法,编译器会搜索为该类型定义的任何扩展方法,并绑定到它找到的第一个扩展方法。以下示例显示了编译器如何确定要绑定到哪个扩展方法或实例方法。
例子
以下示例演示了 C# 编译器在确定是将方法调用绑定到类型上的实例方法还是扩展方法时遵循的规则。静态类包含为任何实现的类型定义的扩展方法。 A、B、C 类都实现了这个接口。
永远不会调用扩展方法,因为它的名称和签名与这些类已经实现的方法完全匹配。
如果编译器找不到具有匹配签名的实例方法,它会绑定到匹配的扩展方法(如果存在这样的方法)。
// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
using System;
public interface IMyInterface
{
// Any class that implements IMyInterface must define a method
// that matches the following signature.
void MethodB();
}
}
// Define extension methods for IMyInterface.
namespace Extensions
{
using System;
using DefineIMyInterface;
// The following extension methods can be accessed by instances of any
// class that implements IMyInterface.
public static class Extension
{
public static void MethodA(this IMyInterface myInterface, int i)
{
Console.WriteLine
("Extension.MethodA(this IMyInterface myInterface, int i)");
}
public static void MethodA(this IMyInterface myInterface, string s)
{
Console.WriteLine
("Extension.MethodA(this IMyInterface myInterface, string s)");
}
// This method is never called in ExtensionMethodsDemo1, because each
// of the three classes A, B, and C implements a method named MethodB
// that has a matching signature.
public static void MethodB(this IMyInterface myInterface)
{
Console.WriteLine
("Extension.MethodB(this IMyInterface myInterface)");
}
}
}
// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
using System;
using Extensions;
using DefineIMyInterface;
class A : IMyInterface
{
public void MethodB() { Console.WriteLine("A.MethodB()"); }
}
class B : IMyInterface
{
public void MethodB() { Console.WriteLine("B.MethodB()"); }
public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
}
class C : IMyInterface
{
public void MethodB() { Console.WriteLine("C.MethodB()"); }
public void MethodA(object obj)
{
Console.WriteLine("C.MethodA(object obj)");
}
}
class ExtMethodDemo
{
static void Main(string[] args)
{
// Declare an instance of class A, class B, and class C.
A a = new A();
B b = new B();
C c = new C();
// For a, b, and c, call the following methods:
// -- MethodA with an int argument
// -- MethodA with a string argument
// -- MethodB with no argument.
// A contains no MethodA, so each call to MethodA resolves to
// the extension method that has a matching signature.
a.MethodA(1); // Extension.MethodA(IMyInterface, int)
a.MethodA("hello"); // Extension.MethodA(IMyInterface, string)
// A has a method that matches the signature of the following call
// to MethodB.
a.MethodB(); // A.MethodB()
// B has methods that match the signatures of the following
// method calls.
b.MethodA(1); // B.MethodA(int)
b.MethodB(); // B.MethodB()
// B has no matching method for the following call, but
// class Extension does.
b.MethodA("hello"); // Extension.MethodA(IMyInterface, string)
// C contains an instance method that matches each of the following
// method calls.
c.MethodA(1); // C.MethodA(object)
c.MethodA("hello"); // C.MethodA(object)
c.MethodB(); // C.MethodB()
}
}
}
/* Output:
Extension.MethodA(this IMyInterface myInterface, int i)
Extension.MethodA(this IMyInterface myInterface, string s)
A.MethodB()
B.MethodA(int i)
B.MethodB()
Extension.MethodA(this IMyInterface myInterface, string s)
C.MethodA(object obj)
C.MethodA(object obj)
C.MethodB()
*/
常用使用模式收集函数
过去,通常创建“集合类”以使给定类型实现 ... 接口并实现该类型集合的功能。创建这种类型的集合对象并没有错,但同样的功能也可以通过对 ... 的扩展来实现。该扩展的优点是允许从实现该类型的任何集合(例如 .Array 或 ...List )中调用函数。使用 Int32 数组的示例可以在 中找到。
图层特定的功能
在使用洋葱架构或其他分层应用程序设计时,通常拥有一组可用于跨应用程序边界进行通信的域实体或数据传输对象。这些对象通常不包含任何功能,或者仅包含适用于应用程序所有层的最小功能。使用扩展方法允许您添加特定于每个应用程序层的功能,而无需使用其他层不需要的方法下载对象。
public class DomainEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
static class DomainEntityExtensions
{
static string FullName(this DomainEntity value)
=> $"{value.FirstName} {value.LastName}";
}
扩展的预定义类型
当我们需要创建可重用的功能时,我们可以扩展现有类型,而不是创建新对象,例如 .NET 或 CLR 类型。例如,在不使用扩展方法的情况下,我们可能会创建或 Query 类来执行针对 SQL 的查询,这些查询可以从代码中的多个位置调用。但是,如果您改为使用扩展方法扩展 .Data.. 类,则可以从连接到 SQL 的任何位置执行查询。其他一些示例可能是向 .类,扩展了 .IO.File、.IO. 和 .IO. 的数据处理功能。对象来实现特定的错误处理功能。这些用例的类型仅受想象力和判断的限制。
使用类型扩展预定义类型可能很困难,因为它们是按值传递给方法的。这意味着将对结构的副本进行任何结构更改。扩展方法退出后,这些更改将不会显示。从 C# 7.2 开始,可以将 ref 修饰符添加到扩展方法的第一个参数中。添加 ref 修饰符意味着第一个参数是通过引用传递的。在这种情况下,可以编写一个扩展方法来改变被扩展结构的状态。
一般准则
虽然通过修改对象的代码或派生新类型来增加功能,但在合理可行的情况下仍然需要扩展方法,但扩展方法已成为在 .NET 生态系统中创建可重用功能的关键选项。对于原始源不受控制、派生对象不合适或不可用,或者功能不应暴露在功能范围之外的情况,扩展方法是一个不错的选择。
有关派生类型的详细信息,请参阅继承。
当使用扩展方法扩展其源代码不受您控制的类型时,您需要接受更改该类型的实现会导致扩展方法无效的风险。
如果您确实为给定类型实现了扩展方法,请记住以下几点:
对于已实现的库,不应使用扩展方法来避免增加程序集的版本号。如果要向拥有源代码的库添加重要功能方法,请遵循 .NET 程序集版本控制指南。有关详细信息,请参阅程序集版本控制。
见