最近花了点时间写了个小程序:媒体中心/MediaCenter。整个过程踩了不少坑,不停在感叹当前的编程环境还是如此艰难。当然你可能会嘲笑我,我写的只是一个桌面级的小应用。不像工程级的大软件那样具备繁杂的内核,无法真正体验不同语言的编码区别。但这正是一个普通编程爱好者的切身感受。海洋很大,里面的鲸鱼数量有限,更多的正是我们这种小鱼小虾。
因为具备一点C#基础,所以在选择语言框架的时候,我一开始还是偏向.net。MVC太老了,于是我尝试了WPF(我当时并不知道WIN UI3和.net MAUI的存在,这个我后面再讲)。是的,你知道在尝试WPF就意味着我一定会接触到MVVM。这里先不管学习MVVM的成本和门槛,你新接触一款框架总要学习的。但是真正让我困扰的是XAML,我当然也会使用成熟的前端框架例如Prism。但当我真正开始搭建前端时,我的感觉是XAML这玩意发明出来就不是给人用的。尤其是当我有一些网络编程的基础,这种感觉尤其强烈。我来举个例子,一个简单的按钮,下面是它的对应代码。
<Button Background="#17181A" BorderThickness="0" Width="50" Height="40" >
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid x:Name="closeButton" Height="40" Width="50" Background="Transparent">
<iconPacks:PackIconControl Kind="{x:Static iconPacks:PackIconMaterialKind.WindowClose}" Width="12" Height="12" Foreground="WhiteSmoke" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="closeButton" Property="Background" Value="Red"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
你敢相信吗,这是一个不涉及任何按钮后端逻辑的一个最基础的按钮样式。仅仅是因为我需要自定义这个按钮的样子,就需要些这么多代码。而如果我给它编辑一个通用样式,然后它会长这样。
<Window.Resources>
<!-- 通用 IconButton Style -->
<Style x:Key="IconButtonStyle" TargetType="Button">
<Setter Property="Width" Value="50"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Background" Value="#17181A"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
<iconPacks:PackIconControl x:Name="Icon"
Kind="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Tag}"
Width="12" Height="12"
Foreground="WhiteSmoke"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<!-- 鼠标悬停 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="RootGrid" Property="Background" Value="#FF3B3B3B"/>
<Setter TargetName="Icon" Property="Foreground" Value="White"/>
</Trigger>
<!-- 鼠标按下 -->
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="RootGrid" Property="Background" Value="#FF5A5A5A"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
你知道吗,在网络前端同样的样式的按钮,代码量差不多是上面的十分之一。这里我们还没说关于前后端通讯的各种坑。总之我到了这一步决定换框架了。我查了一下当前主流的桌面程序框架,发现应用网络前端作为桌面前端的模式已经成为主流。于是最终选择了tauri2+vue3,而这是另一个噩梦的开始。首先前端我认为没有任何问题,即简洁又好用。但是我严重低估了rust语言的繁杂。
如果你查询网络可能会发现这样一种言论,说rust语言是最好的编程语言,对此我真的不敢苟同。这里我要说一下rust语言最著名的两个特性。第一个显示所有可能的错误,这个特性被rust官方包装成rust语言是最安全的编程语言。但真正的结果是这样
if let Some(caps) = someRegex.captures(someString) {
let min: f64 = caps.get(1).unwrap().as_str().parse().unwrap_or(0.0);
// 其他代码
}
首先第一行someRegex.captures(someString),把字符串匹配一个已经设定好的正则表达式。那为什么要有if let Some(caps)呢?因为这种匹配可能会报错,所以if let是为了区分在不报错的情况下该执行的逻辑。然后第二行,caps.get(1)是取出匹配正则表达式后的生成的类似数组结构中的第一个元素。为什么要unwrap()呢?因为有可能取不到值,所以这里我告诉编译器一定能取到的不要担心。as_str().parse()这里是把元素先转换成类似字符串的格式,再转换成数字。为什么后面又有unwrap_or(0.0)呢?因为parse也可能报错,所以我要告诉编译器它如果报错了,你就当做0来处理。能体会到费劲吗?一个很单间的语句
let min: f64 = someRegex.captures(someString).get(1).as_str().parse()
就是rust语言并不关心你的实际代码逻辑是否真的会出现报错,而只是粗暴的定义某些(绝大部分)方法就是存在报错的可能性,那么你在使用它们的时候就必须做报错判断。而这种方法遍布整个代码中。
然后是第二个特性,变量所有权。这里我就不再举例说明了。你只需要理解,如果一个变量在一个方法中被读取了一百次,你每一次都需要额外注意这个变量还有没有所有权。而你付出的所有这些代码成本和精神成本换来的是什么呢?不是代码效率提高了,而只是编译器不报错。当然,你可能会说(网络上有很多人都这么说)这是为了安全考虑。但问题是我只是在写一个桌面小程序,我没有在构建一个操作系统。而这个语言为了达到程序运行时尽量少报错的措施是将所有问题解决推给用户,而这种粗暴的做法仅仅是在解决表面问题。到底是什么导致了程序报错,不是用户代码中有没有写报错信息,而是用户真正在写代码时使用的错误逻辑。当然这些都是rust语言本身的问题,至于tauri。基于webview的前端为用户带来了很好的编程体验,html+css足够简单易上手,足够简洁符合直觉,vue等前端框架提供了非常灵活的编程体验。但这也导致了一个网络前端的对应本地程序不应有的限制。比如权限限制,webview会天然的阻止你使用某些方法。另外,你无法像传统程序那样调用内存空间直接进行数据传递,这在大文件(比如上百MB)的传输上造成了很多麻烦。
至于前面提到的WIN UI3和.net MAUI,我只想表达,微软你要不放弃XAML,做啥改进都是徒劳的。市场已经证明人们在对编程的追求上永远都是追求减法。想想你为什么在C/C++后推出了C#,想想你的浏览器。
所以现在就没有一个足够好用的整体框架供我使用吗?Qt?