为什么要为类编写扩展方法而不是直接更改实现?

时间:2019-03-08 17:17:16

标签: c#

为什么要为类编写扩展方法而不是直接更改实现?

例如,如果我有课程

public class A 
{
    public void someMethod1() {...}
}

,我想添加更多功能,那为什么还要编写一个扩展方法,如:

public static class AExtensions 
{
    public static void someMethod2(this A a) {...}
}

不仅仅是将实现直接更改为

public class A 
{
    public void someMethod1() {...}
    public static void someMethod2() {...}
}

扩展方法似乎比较麻烦。

3 个答案:

答案 0 :(得分:7)

这个问题很奇怪;似乎是“当我知道如何以简单的方式做事时,为什么我会以困难的方式做事?”您不会,所以问为什么会是一件很奇怪的事情。您通常在控制扩展类型时将 not 放入扩展方法中。在这种情况下,并未发明扩展方法。它们是针对相反场景而发明的,在这种情况下,进行扩展的人员控制类型。

但是,让我们来看看原始的发布者,看看在某些情况下即使在我们控制所有定义的情况下,将功能放入扩展方法中也是合理的。对我来说,目前尚不清楚为什么原始海报正在寻找使用此模式的借口,但我们不必为此担心。

这里是一个。遵循逻辑:

  • 让我们假设我们有一个通用类型,希望以协变或逆变方式使用。
  • C#仅支持接口和委托上的通用变量,因此变体部分必须进入接口。
  • C#中的接口尚不能支持实现。 (这是我在C#中缺少的少数Java功能之一,并且已被提议用于该语言的未来修订版。在我撰写本文时,该功能处于C#8的测试版本中。)
  • 特征签名会阻止所需的方差注释。
  • 所需的功能可以实现为扩展方法。

在这种情况下,即使我们控制类和接口,也可以将功能实现为扩展方法

那可能很抽象。让我们看一个例子。我们希望实现一个可变的不可变堆栈。这是我们的首次尝试:

public abstract class Stack<T>
{
    private Stack() {}
    public static readonly Stack<T> Empty = new EmptyStack();
    private sealed class EmptyStack : Stack<T>
    {
      public override bool IsEmpty => true;
      public override T Peek() => throw new Exception("Empty stack");
      public override Stack<T> Pop() => throw new Exception("Empty stack");
    }
    private sealed class Regular : Stack<T>
    {
      private readonly T head;
      private readonly Stack<T> tail;
      public Stack(T head, Stack<T> tail)
      {
        this.head = head;
        this.tail = tail;
      }
      public override bool IsEmpty => false;
      public override T Peek() => head;
      public override Stack<T> Pop() => tail;
    }
    public abstract bool IsEmpty { get; }
    public abstract T Peek();
    public abstract Stack<T> Pop();
    public Stack<T> Push(T head) => new Regular(head, this);
}

好的,第一个问题:我们不能在类类型上使用方差。它必须是一个接口。什么界面?

public interface IStack<out T>
{
    bool IsEmpty { get; }
    IStack<T> Pop();
    T Peek();

到目前为止,协方差没有问题。如果我们有IStack<Mammal>,则可以将其用作IStack<Animal>,因为当我们偷看一堆狮子,老虎和熊时,每次都会得到一只动物。

但是Push呢?我们不能写

    IStack<T> Push(T t);

因为现在T的使用位置无效。

但是让我们考虑一下。假设我们有一个IStack<Turtle>我们可以将长颈鹿推上去吗?听起来错了;您不能将长颈鹿放入海龟列表中,那么为什么您应该能够将长颈鹿推到一堆海龟上呢?

但这确实有效:一堆乌龟就是一堆动物,我们可以将长颈鹿推到上面。所以我们需要的是:

public interface IStack<out T>
{
    ...
    IStack<U> Push<U>(U u) where T : U;
}

在C#中是非法的;没有这样的“向后”约束。(再次,Java可以做得更好的少数几个领域之一。)

好的,我们在界面中没有我们想要的Push。那我们该如何推入堆栈呢?我们可以通过对类型进行少量更改来做到这一点:

public abstract class Stack<T> : IStack<T>
{
    private Stack() {}
    public static readonly Stack<T> Empty = new EmptyStack();
    private sealed class EmptyStack : Stack<T>
    {
      public override bool IsEmpty => true;
      public override T Peek() => throw new Exception("Empty stack");
      public override IStack<T> Pop() => throw new Exception("Empty stack");
    }
    private sealed class Regular : Stack<T>
    {
      private readonly T head;
      private readonly IStack<T> tail;
      public Stack(T head, IStack<T> tail)
      {
        this.head = head;
        this.tail = tail;
      }
      public override bool IsEmpty => false;
      public override T Peek() => head;
      public override IStack<T> Pop() => tail;
    }
    public abstract bool IsEmpty { get; }
    public abstract T Peek();
    public abstract IStack<T> Pop();
    public static IStack<T> Push(T head, IStack<T> tail) => 
      new Regular(head, tail);
}

超级。呼叫站点是什么样的?

IStack<Turtle> s1 = Stack<Turtle>.Empty;
IStack<Turtle> s2 = Stack<Turtle>.Push(someTurtle, s1);
IStack<Animal> s3 = Stack<Turtle>.Push(anotherTurtle, s2);
IStack<Animal> s4 = Stack<Animal>.Push(someGiraffe, s3);

完全有效。我们用一堆乌龟作为一堆动物,然后将长颈鹿推上去。但是,噢,我的天哪,看看那个呼叫站点有多可怕!

我们需要的是一种将类型参数从调用站点中移出的方法,但是我们可以使用...一种扩展方法来做到这一点!

public static IStack<T> Push<T>(this IStack<T> s, T t) => 
  Stack<T>.Push(t, s);

现在我们的呼叫站点是什么样的?

IStack<Turtle> s1 = Stack<Turtle>.Empty;
IStack<Turtle> s2 = s1.Push(someTurtle);
IStack<Animal> s3 = s2.Push(anotherTurtle);
IStack<Animal> s4 = s3.Push(someGiraffe);

好多了。更好:

var s3 = Stack<Turtle>.Empty.Push(someTurtle).Push(anotherTurtle);
var s4 = s3.Push((Animal)someGiraffe);

(强制转换的要求有点不幸; C#类型推断不会得出“我有一只乌龟和长颈鹿,开发者可能是动物”的信息。相反,它会得出“我有一只乌龟和长颈鹿,并且我不知道该选择哪个。”强制转换可帮助编译器解决歧义。)

因此,要回答您的问题:为什么可以在修改类时实施扩展方法? 如果修改类使您陷入呼叫站点必须丑陋的情况,那么您会这样做,但是添加巧妙的扩展方法可以带来愉悦,流畅,令人愉悦的用户体验。

答案 1 :(得分:2)

这不仅与您无权修改自己的类有关,而且extension methods也可以在接口上工作,在接口上,事情会变得非常有趣(并且会带来许多LINQ优点)来自)。

因此,如果您有一个名为#define MAX_FILE_SIZE 0x10000000 void ExtendFileSection() { HANDLE hFile = CreateFile(L"d:/ee.tmp", GENERIC_ALL, 0, 0, CREATE_ALWAYS, 0, 0); if (hFile != INVALID_HANDLE_VALUE) { HANDLE hSection; SYSTEM_INFO info; GetSystemInfo(&info); // initially only 1 page in the file LARGE_INTEGER SectionSize = { info.dwPageSize }; NTSTATUS status = NtCreateSection(&hSection, SECTION_EXTEND_SIZE|SECTION_MAP_READ|SECTION_MAP_WRITE, 0, &SectionSize, PAGE_READWRITE, SEC_COMMIT, hFile); CloseHandle(hFile); if (0 <= status) { PVOID BaseAddress = 0; SIZE_T ViewSize = MAX_FILE_SIZE; //MapViewOfFile(hSection, FILE_MAP_WRITE|FILE_MAP_RESERVE, 0, 0, MAX_FILE_SIZE); status = ZwMapViewOfSection(hSection, NtCurrentProcess(), &BaseAddress, 0, 0, 0, &ViewSize, ViewUnmap, MEM_RESERVE, PAGE_READWRITE); if (0 <= status) { SIZE_T n = MAX_FILE_SIZE / info.dwPageSize - 1; do { SectionSize.QuadPart += info.dwPageSize; if (0 > NtExtendSection(hSection, &SectionSize)) { break; } } while (--n); UnmapViewOfFile(BaseAddress); } CloseHandle(hSection); } } } 的接口,则可以定义以下扩展类:

IEnumerable<T>

现在,只要有实现IEnumerable的任何地方,该扩展方法就可以使用。

您可以执行以下操作:public static class EnumerableExtensions { public static IEnumerable<T> ForEach(this IEnumerable<T> enumerable, Action<T> action) { foreach (T item in enumerable) { action(item); } return enumerable; } }

但是请注意我的扩展方法如何返回list.ForEach(x => Console.WriteLine(x.SomeProperty))?这就是method chaining出现的地方:

IEnumerable<T>

答案 2 :(得分:1)

您可以在无权访问的类上使用扩展方法,例如,可以将扩展方法添加到String中,并且不能修改String,因为它是.NET的一部分。这也意味着您不必一直拥有它们-例如LINQ,它是扩展方法的集合,仅当您添加using语句时才可用,这样它们才不会不必要地阻塞。它还使代码保持整洁,编写名为Reverse的扩展方法,并且执行"Hello".Reverse();MyExtensionClass.Reverse("Hello");更好,尤其是当它在类似ASP.NET页面的页面上出现50次时,通常必须处理渲染复杂模型时使用。