在C#中创造性地使用扩展方法

哈Ha!



继续探索C#主题,我们为您翻译了以下有关扩展方法原始用法的简短文章。我们建议您特别注意有关接口的最后部分以及作者的个人资料







我确信任何具有一点C#经验的人都知道扩展方法的存在。这是一个很好的功能,允许开发人员使用新方法扩展现有类型。



如果要向不受控制的类型添加功能,这非常方便。实际上,任何人迟早都要为BCL编写扩展名,只是为了使事情更易于访问。



但是,除了相对明显的用例外,还有一些非常有趣的模式与扩展方法的使用直接相关,并演示了如何以非传统方式使用它们。



在枚举中添加方法



枚举仅是常量数字值的集合,每个值均分配有唯一的名称。尽管C#中的枚举从抽象类Enum继承,但它们不解释为真实类。特别是,此限制使他们无法使用方法。



在某些情况下,将逻辑编程为枚举可能会有所帮助。例如,如果枚举值可以存在于多个不同的视图中,而您想轻松地将一个转换为另一个。



例如,在典型的应用程序中设想以下类型,该类型允许您以各种格式保存文件:



public enum FileFormat
{
    PlainText,
    OfficeWord,
    Markdown
}


该枚举定义了应用程序支持的格式列表,可用于应用程序的不同部分,以基于特定值启动分支逻辑。



由于每种文件格式都可以表示为文件扩展名,因此,如果每个文件格式FileFormat都有一种获取此信息的方法,那就太好了。可以通过扩展方法来完成,如下所示:



public static class FileFormatExtensions
{
    public static string GetFileExtension(this FileFormat self)
    {
        if (self == FileFormat.PlainText)
            return "txt";

        if (self == FileFormat.OfficeWord)
            return "docx";

        if (self == FileFormat.Markdown)
            return "md";

        //  ,      ,
        //      
        throw new ArgumentOutOfRangeException(nameof(self));
    }
}


反过来,这允许我们执行以下操作:



var format = FileFormat.Markdown;
var fileExt = format.GetFileExtension(); // "md"
var fileName = $"output.{fileExt}"; // "output.md"


重构模型类



有时候,您不想直接向类添加方法,例如,如果您使用的是贫血模型



贫血模型通常由一组公共不可变属性(仅获取)表示。因此,在将方法添加到模型类中时,您可能会感到违反了代码的纯正性,或者您可能怀疑这些方法引用了某种私有状态。扩展方法不会导致此问题,因为它们无法访问模型的私有成员,并且本质上也不是模型的一部分。



因此,请考虑下面的示例,其中有两个模型,一个模型代表一个封闭的标题列表,另一个模型代表一个单独的标题行:



public class ClosedCaption
{
    //  
    public string Text { get; }

    //       
    public TimeSpan Offset { get; }

    //       
    public TimeSpan Duration { get; }

    public ClosedCaption(string text, TimeSpan offset, TimeSpan duration)
    {
        Text = text;
        Offset = offset;
        Duration = duration;
    }
}

public class ClosedCaptionTrack
{
    // ,    
    public string Language { get; }

    //   
    public IReadOnlyList<ClosedCaption> Captions { get; }

    public ClosedCaptionTrack(string language, IReadOnlyList<ClosedCaption> captions)
    {
        Language = language;
        Captions = captions;
    }
}


在当前状态下,如果需要获取在特定时间显示的字幕字符串,我们将像这样运行LINQ:



var time = TimeSpan.FromSeconds(67); // 1:07

var caption = track.Captions
    .FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);


这确实是乞求某种可以作为成员方法或扩展方法实现的辅助方法。我更喜欢第二种选择。



public static class ClosedCaptionTrackExtensions
{
    public static ClosedCaption GetByTime(this ClosedCaptionTrack self, TimeSpan time) =>
        self.Captions.FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);
}


在这种情况下,扩展方法可以使您获得与通常方法相同的效果,但是会带来许多非显而易见的好处:



  1. 显然,此方法仅适用于该类的公共成员,并且不会以某种神秘的方式更改其私有状态。
  2. 显然,此方法只允许您偷工减料,此处提供此方法仅是为了方便。
  3. 此方法属于一个完全独立的类(甚至是汇编),其目的是将数据与逻辑分离。


通常,使用扩展方法时,在必要和有用之间划清界限是很方便的。



使接口通用



当设计接口时,您总是希望合同尽可能小,因为它使实施更容易。当界面以最通用的方式提供功能时,它会很有帮助,以便您的同事(或您自己)可以在该界面上构建以处理更具体的情况。



如果这听起来很无聊,请考虑将模型保存到文件的典型接口:



public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);
}


一切工作正常,但是可能在几周后提出新的要求:实现的类IExportService不仅必须导出到文件,而且还必须能够写入文件。



因此,为了满足此要求,我们在合同中添加了一种新方法:



public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);

    byte[] SaveToMemory(Model model);
}


这项更改刚刚破坏了所有现有的实现IExportService,因为它们现在都需要更新以支持写入内存。



但是,为了不做所有这些事情,我们可以从一开始就设计界面有些不同:



public interface IExportService
{
    void Save(Model model, Stream output);
}


在这种形式下,界面会强制您以最通用的形式(即this)编写目标Stream。现在,我们不再局限于工作时的文件,而且还可以定位其他各种输出选项。



这种方法的唯一缺点是,最基本的操作并不像我们以前那样简单:现在,我们必须设置一个特定的实例Stream,将其包装在using语句中并将其作为参数传递。



幸运的是,使用扩展方法时,此缺点已完全消除:



public static class ExportServiceExtensions
{
    public static FileInfo SaveToFile(this IExportService self, Model model, string filePath)
    {
        using (var output = File.Create(filePath))
        {
            self.Save(model, output);
            return new FileInfo(filePath);
        }
    }

    public static byte[] SaveToMemory(this IExportService self, Model model)
    {
        using (var output = new MemoryStream())
        {
            self.Save(model, output);
            return output.ToArray();
        }
    }
}


通过重构原始接口,我们使它更具通用性,并且没有通过使用扩展方法牺牲可用性。



因此,我发现扩展方法是保持简单简单并将复杂变为可能的宝贵工具



All Articles