C#14 的 Extension members 之感想
C#14 的 Extension members 之感想
extension关键字的介绍和使用,可以在官方代码库找到:Extension members。
在这里,我们不去探讨前因,当下,我们只讨论未来。大家可能觉得这是一个挺不错的功能,但是我研究了一下,的确结合之前的一些技术,感觉它能带来很多的突破。
类型转换逻辑的思想优化
大家对类型转换的关键字,大约最有印象的是三个: Convert | ToInt32 | Parse。下面我为大家解析一下我对这三个关键字引申的函数的看法。
下面开始分三个转换函数,传入string,返回int,做详细的用法解析。
int result = Convert.ToInt32(stringValue);
上方的Convert是函数的调用起点,由于返回值类型和结果 int 并无关联,所以这属于"字典式"用法,对document有足够的认知,才能知道函数在哪个位置内,这是缺点。
之后的方法名,是和我们的返回值"近似"的,但是这只能算缺点,因为返回值类型一旦复杂,方法名便不能完整表达返回值的类型意义。
如果我们不仅仅是string和int之间的转换,实现代码是可以集中的,如果我们将实现的复杂度和调用的复杂度综合,可以确认实现方是很集中的,算优点。调用方是需要熟悉成本的,但是在参数类型多变,可能不只是string时,才有这个唯一优势。
返回值在这里能看到是加了int标识的,这是缺点,因为右侧无明确的类型标识。这也是编辑器里推荐的做法,如果你写了 var 关键字,就更能反映出右侧函数体的不明确性。
int result = stringValue.ToInt32();
上方的第二种转换,是通过为实例扩展方法完成的,它调用相对简单,唯一的学习成本就是确认命名空间是可用的,这是扩展方法的优点。
扩展方法的使用优先级,是要低于类型定义方法的,所以 string 本身由于使用量过大,滥用扩展方法很可能的结局就是污染了代码编写效率,在代码感知和智能提示功能上,算是很明显的弱点了。这些许会通过IDE的功能分组来解决,但是至今没明确的优化方案。
同理,方法名也无法明确和返回类型对应,说不定你光想方法名都得想破头,的确是缺点。
var result = Int32.Parse(stringValue);
上方的第三种转换,是理论上较为清晰的最优写法。它的入口就是返回值类型,不需要额外的学习成本。方法名也只需要动词,不需要额外的附加类型之类的。返回值类型也可以明确用 var 忽略。
为什么我在这里要明确的指定第三种转换呢,接下来基于extension的扩展内容,便是它最好的实现了。
var result = Student.ParseFromXml(xmlString);
这里能看出,通过extension的方式,扩展了针对类型的静态方法。结果也显而易见。调用的入口直接就是结果的类型,方法名可以很简略,也可以针对入参添加区分。的确没有明显的缺点,这简直就是史诗级别的进步! 因为我可以为官方库、第三方类库、甚至我不想碰旧代码,就能达到这一点,虽然你看到的没有眼花缭乱炫目多彩,但是这绝对能改变原有的写作风格,而你的思想也会有所改变。
为什么我要重点说明思想转变这个词儿呢,这么简单的语句到底能代表什么? 后续我会有更多示范为你呈现。
我需要什么,和我如何得到
我们先准备一个最基本的方法定义,之后为你描述一下代码思想上的细节。
Student ParseFromXml(string xmlString) 
{
    student stu;
    // stu = read string and deserialization
    return stu;
}
上方的方法定义大家都不陌生,我们的关注点在于,这一个区间的代码入口是什么,出口是什么,中间的逻辑不用细看,大体上猜一猜做了啥就可以了,一般大家都是这么想的,也希望尽可能的做到这一点,基于此多了很多名词,什么职责链,最小功能,防止面条,能想到的你都可以捋一遍,至此,我们有了对它的很好的回顾。
var client = new HttpClient();
var content = new StringContent();
HttpResponseMessage response = await client.PostAsync(stringUrl, content);
var jsonResponse = await response.Content.ReadAsStringAsync();
上方我们用一个反向的例子,说明一下在一个代码区间里,无法明确调用起点,也无明显的出入口的例子,这个从WebAPI获取数据就是很常见的。当然我们不会去写出非常细致的代码,也是为了节省大家的心力。
在这段代码里,我们会觉得,入口可能是client实例,但是你想得到入口,你得先得到这个实例,所以作为入口它是反直觉的。没错,的确有点儿不对劲却无法描述,这就和我假设以jsonResponse为返回值,却最后才能接触到它一样,那我如果以它作为入口呢? 如果client可以重用,那一切都变得很好了,但是和上方列出的方法体一样,我所在意的入口和出口到底是在哪里呢? 我是有些混乱的,代码太多我不想看。
```csharp
string PostAndReturnJson(string contentString) 
{
    // Request and Response-Code (和上一个代码示例接近)。
    return jsonResponse;
}
```
为了让这混乱不迷了我的眼睛,我用一个方法把它套起来吧,就像上面介绍的一样,然后我就解放了,再也不用看client,content,request,response,真的很惬意了。
但是里面的逻辑到底能不能再优化、更多更能、精华、复用,我的确是想起来就头大,因为我的确不想看到这里面的逻辑了,万一我改坏了代码执行顺序,肯定是一团糟。
var client = new HttpClient();
string jsonResponse = client.PostStringAsync(contentString)
    .ReadStringAsync();
SaveState modelResponse = client.PostJson(student)
    .ReadAs<SaveState>();
如上所示,为了增加灵活性,我们想到了 ‘Fluent API’ 类似的方式,它的确变得简单了,利用组装的能力,我们能在不改变核心内容的情况下,写出我还觉得顺手的代码,这很优秀。不过我们在这里也要感谢愿意组件化逻辑的提供人员。
不过在这里,我们还是有一些遗憾,我会意识到,我最开始的脑海中,就是应该以返回值作为入口,这能不能做到呢,接下来我们随便整点儿花样,试想一下新花样。
// 这是最理想的,不过的确需要更多支撑,比如 SaveState 这种的类型有共同的父级,统一DTO模型的基类。
var modelResponse = SaveState.LoadByPostJson(student);
// 稍微次之,不过也能体现更多细节
var modelResponse = Response<SaveState>.LoadByPostJson(student);
大家也许能感觉出有点儿不一样的味道了,我们不看其他的,把关注点放在一段可以组合的逻辑的最开始,它就是我的重点,就很像我们函数的return,而后面的就是函数的剩余部分,这的确让我们不习惯,不过我相信你细细品味,就能知道它的好处!
我要是能直接扩展类型成员呢?
不过前面的代码示例,并没有体验出 extension 这个语言关键字的作用,看起来不用他也能正常使用,是吧? 接下来我说明一下它存在的一个重点。
// .NET 通用主机 的示范
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// ASP.NET Core Web 主机 的示范 (微调,精简了代码)
IWebHostBuilder builder = WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
// Avalonia 项目的入口
    public static AppBuilder BuildAvaloniaApp()
        => AppBuilder.Configure<App>()
            .UsePlatformDetect()
            .WithInterFont()
            .LogToTrace();
我这里贴出三个思想上完全一致,但是却死活都对不到一起的三个 ‘入口’。大家看到,三个入口类型,竟然都不一样! 这时候脑子里的第一个想法是,他们是不同的项目嘛,那类的位置也都是不一样的,类名也随便起就是喽,无所谓了。
现在有了 extension 关键字,让类型支持扩展成员,那结果就不一样了,它完全可以起到和接口类似的功效!
// .NET 通用主机 的示范
var builder = Builder.CreateApplicationBuilder(args);
// ASP.NET Core Web 主机 的示范 (微调,精简了代码)
var builder = Builder.CreateWebBuilder(args);
// Avalonia 项目的入口
var builder = Builder.BuildAvaloniaApp(args);
没错,真的在逻辑的入口,可以完全一致! 和接口一样的功效,但是用法更精简。大家可能会说左边你都把类型去掉了会不会引起误会呀? 有这个可能,但是项目的这个位置的 builder 的确没啥区别,很少有人会关心赋值等号左侧的类型到底有啥不一样,大家更关心的的确是右侧,右侧写法竟然一致,这就可以很好的为跨端跨项目甚至跨平台合并逻辑了。
大家可不要小看这一点,仅仅从程序的初始化,就能得出配置统一的重要性。你的web程序,和客户端程序,想不想有99.99%的代码一样? 那这一点绝对是必须的。因为很多的map、log、db、service,都是可以平台通用的,至今写法却都不一样!
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号