我从前吗很讨厌C#

这是我的主要就词汇,每天我将它与其它盛行词汇展开较为时,我很开心我不留神优先选择了它。 Python 和 Javascript 缺少动态类别(假定 JS 有任何人类别的类别),Java 缺乏适度的C#、特性、该事件、值类别,这引致大部份那些包装袋类如 Integer 等一塌糊涂。
我不得已提的是,我而已在较为词汇这类和采用它撰写的宽敞感,而没囊括辅助工具和其它自然环境的主轴,即使那些都足够多好,能使民营企业合作开发显得科学合理和宽敞。
接着我所致疑惑试著了 F#。
好的,那有甚么益处呢?
详述如下表所示:
预设相对性
在我看来,表达式式本体论比他们那时所言的 OOP 更可信、更简约
请降类别或可界定联手
排序表达式
SRTP 或动态解析类别参数
null 安全
简约的句法
类别推断
好吧,null 部分很简单:没甚么比像 Task<IEnumerable<Employee>> 那样无休止地检查返回值更让代码如同天书了。 因此,让他们谈谈相对性和简约性。 考虑以下 POCO 类:
public class Employee{ public Guid Id { get; set; }public string Name { get; set; } public string Email { get; set; } public string Phone { get; set; }public bool HasAccessToSomething { get; set; } public bool HasAccessToSomethingElse { get; set; }}
简短,简单,没多余的东西。 不能再短了。 现在看看 F# 等效代码:
type Employee ={ Id: Guid Name: string Email: string Phone: string HasAccessToSomething: boolHasAccessToSomethingElse: bool }
现在吗没多余的东西了。 有用的信息包含在类别关键字、类别名称、字段名称和类别中。 在 C# 中在每一行有无用的 public 和 { get; set; } 。 除此之外,在 F# 中他们还有 null 安全性和相对性。 好吧,实际上他们也能在 C# 中获得相对性,至于公共特性——自动完成不是一个大问题:
public class Employee
{
public Guid Id { get; }
public string Name { get; }
public string Email { get; }
public string Phone { get; }
public bool HasAccessToSomething { get; }
public bool HasAccessToSomethingElse { get; }public Employee(Guid id, string name, string email, string phone, bool hasAccessToSmth, bool hasAccessToSmthElse)
{
Id = id;
Name = name;
Email = email;
Phone = phone;
HasAccessToSomething = hasAccessToSmth;
HasAccessToSomethingElse = hasAccessToSmthElse;
}
}完毕! 虽然,代码量增加了两倍——大部份字段都重复了两次。 更重要的是,如果他们添加一个新字段,他们很容易忘记将它添加到构造表达式参数和/或初始化它,编译器不会说任何人事情。 另一方面,在 F# 中,当他们添加一个新字段时,他们所要做的就是添加它。 仅此而已。 初始化看起来像这样:
let employee ={ Id = Guid.NewGuid() Name = “Peter” Email = “peter@gmail.com” Phone = “8(800)555-35-35”HasAccessToSomething = true HasAccessToSomethingElse = false}
如果他们错过了一个字段,代码将无法编译。 由于此对象是不可变的,因此展开更改的唯一方法是创建另一个对象。 但是,如果他们只需要更改一个字段怎么办? 小菜一碟!
let employee2 = { employee with Name = “Bob” }
在 C# 中做到这一点? 我想你已经知道了。此外,那个{get;} 除非内部对象这类是不可变的,否则事情就不妙了,所以你也要顾及他们。 我甚至还没提到集合?
但他们吗如此需要相对性吗?
我是故意添加那些access字段的。在实际项目中,通常有一个access服务负责它,并且它经常接收一个模型并对其展开修改,在需要的地方将access字段设置为 true。 所以在我的程序中的某个时刻,我得到了这个模型并且access字段被设置为 false。 这是甚么意思? 可能是该模型尚未通过该服务,或者可能是某些字段忘记处理,或者可能而已员工没访问权限 – 这需要我检查它并阅读大量代码。
但是当结构不可变时,我知道一切都很好,即使编译器强制我在声明时完全初始化对象。 在其它情况下,添加新字段后我必须:
检查创建这个对象的大部份地方——也许我应该在那里填写那些字段
检查大部份服务,改变这个对象
撰写/更新与此对象相关的单元测试
可能需要更新映射
处理不可变对象时,您还能确定其它代码或线程不会破坏它的内部。 但在 C# 中,很难获得真正的相对性,以至于撰写不可变代码不值得付出努力,你不需要以这种代价获得相对性。
但足够多了,他们还有甚么? 在 F# 中,他们还免费获得了:
结构相等
结构较为
所以现在他们能这样做:
if employee1 = employee2 then//…
而这段代码将完全符合他们的意思——真正的相等。 通过引用较为对象的 Equals 是纯粹多余的,他们已经有了 Object.ReferenceEquals,谢谢。
有人可能会争辩说没人需要它,即使在现实生活中他们很少较为对象,因此手动覆盖 Equals 和 GetHashCode 没甚么大不了的。 但我认为因果关系在这里颠倒了:他们不较为常规对象,即使手动覆盖和维护 Equals 和 Compare 需要付出巨大的努力。 然而,当它免费提供时,能立即找到用途:您能将它放入 HashSet 或 SortedSet,将它用作 Dictionary 中的键并且不要通过它的 id 较为对象,而而已较为它(尽管 id 选项是 当然仍然可用)。
界定联手
我想他们中的大多数人用膝盖想都知道,基于异常构建工作流是错误的。 例如,不是采用 try { i = Convert.ToInt32(“5”); } catch(Exception){} 最好采用 int.TryParse。 但是除了这个原始和不堪的例子之外,他们不断地打破这个规则。 用户提供了错误的输入? 验证异常! 超出数组范围? 索引超出范围异常!
在有些书中,他们说例外是针对特殊的、不可预测的情况,当出现严重错误以致于试图继续他们的工作根本没意义时。 OutOfMemoryException、StackOverflowException、AccessViolationException 等都是很好的例子。 但是在数组中越界是不可值远远多于正常工作的值。 这意味着给定一个 int 的随机值,它更有可能发生异常情况! 现在,我知道没人在数组中采用随机索引,每个人都检查它的长度等等。 难怪! 还是很有代表性的。 想象一下,他们正面对的是某种类别输入的抽象表达式,但随后您发现那些值中的大多数都会引致异常。 有点烦人,不是吗?
验证也是如此。 用户提供了错误的数据? 真是个惊喜。
这种异常滥用的原因很简单:类别系统不够强大,无法表示“如果一切正常,就给我一个结果,否则返回一个错误”这样的场景。 严格类别要求他们在每个执行分支中返回相同的类别。 但是向他们大部份的模型添加字符串 ErrorMessage 和 IsSuccess 是最不需要的。 因此在 C# 中,异常可能是邪恶。 当然你能这样做:
public class Result<TResult, TError>{public bool IsOk { get; set; } public TResult Result { get; set; } public TError Error { get; set; }}
但是这里他们又需要写很多代码来使无效状态失效。 否则他们能把result和error都初始化,却又忘记设置IsOk,这样带来的问题吗比解决的问题多。
在F#中,你能更容易地定义那些东西:
type Result
| Ok of TResult
| Error of TErrortype ValidationResult
| Valid of TInput
| Invalid of string listlet validateAndExecute input =
match validate input with //检查验证结果
| Valid input -> Ok (execute input) // 如果有效,返回执行结果
| Invalid of messages -> Error messages // 如果没,他们返回一个错误消息列表就是这么简单、简约,最重要的是,代码是自文档化的。您不必撰写xml文档并指定方法在某些情况下抛出异常,也不必用try/catch包装袋其它方法调用以防万一。在这种类别的系统中,异常发生在真正危险和错误的情况下。
当你总是在这里或那里抛出异常时,你需要一个复杂的错误处理。 现在你给自己一个 BusinessException 或 ApiException,接着你必须从它继承大量的异常并继续跟踪每个采用那些正确的异常,如果有人犯了错误 – 客户将得到 500 而不是 404 或 403。现在你不得已枯草的查看日志,挖掘堆栈跟踪。
如果他们没遍历匹配表达式中的大部份情况,F# 编译器会发出警告。
type UserCreationResult = | UserCreated of id:Guid| InvalidChars of errorMessage:string | AgreeToTermsRequired | EmailRequired | AlreadyExists
现在他们看到了操作的大部份可能结果,这比异常列表更具代表性。 不仅如此,当他们根据新要求添加新案例 AgreeToTermsRequired 时,F# 编译器会在他们处理此结果的位置发出警告。
所致显而易见的原因,我从未在项目中见过如此详细的异常描述列表。 那些场景是在文本消息中为那些例外情况定义的。 结果,当合作开发人员显得懒惰并使那些消息更加抽象时,他们偶尔会收到重复消息。
顺便说一下,数组索引现在看起来也更漂亮了,没 if/else 和长度检查:
let doSmth myArray index = match Array.tryItem index myArray with| Some elem -> Console.WriteLine(elem) | None -> ()
这里他们采用标准库中的类别:
type Option<T> = | Some of T | None
这是 null 或缺失值案例的更好替代方案。 每当他们看到它时,他们就知道,该值能根据要求缺失,而不是即使合作开发人员的错误。 再一次,编译器会检查你的代码,让你检查大部份可能的情况。
实体本体论
纯表达式和基于表达式的词汇设计迫使他们撰写极其稳定的代码。 纯表达式满足以下条件:
表达式没副作用,唯一的执行结果是评估输出
表达式总是对相同的输入产生相同的输出
在此基础上加上整体性(当表达式为每个可能的输入产生正确的输出时),您将获得可信的、可预测的线程安全代码,它始终能正常工作。
基于表达式的设计告诉他们,万物皆表达式,万物皆有执行结果。 例如:
let a = if someCondition then 1 else 2
编译器强制他们写大部份可能的分支,你不能在 if 上停下来,你需要 else,否则代码将无法编译,所以不可能忘记任何人一个分支。
在 C# 中,你有做同样事情的三元运算符,但也很容易撰写不安全的代码,当你定义了一些东西,改变了它的某些部分,接着你错过了一些东西。
远离熟悉的 OOP
常见情况:你有一个服务,它依赖于其它一些服务和一个存储库。 那些服务有自己的依赖关系。 现在,大部份那些都通过强大的 DI 框架混合在一起,形成令人讨厌的功能混合物,并移交给 Web 控制器。
服务的每个依赖项平均有 2-5 个依赖项,每个依赖项,平均有 3-5 个方法。 当然,它中的大多数并没在每个特定场景中采用。 在每个特定情况下,在大部份这个巨大的方法树中,他们需要每个依赖项的 1-2 个方法,但无论如何他们将它结合在一起并创建许多对象。 当然还有Mock。 你会怎么想? 他们确实必须测试大部份那些,不是吗? 所以现在我想测试我的服务方法。 为了调用它,我需要该服务的一个对象。 要创建该对象,我必须通过mock。 诀窍是知道我需要甚么特定的mock:其中一些mock没在这里被调用,所以我不需要它,他们需要的而已它中的几个方法。 所以每天我都为我的测试设置了繁琐的步骤,包括指定返回值和大部份那些东西。 接着我想用同样的方法测试另一个案例。 重新设置(彻底疯狂)! 有时,方法测试中的代码多于方法这类。 哦,是的,对于其它大部份方法,我都必须深入其内部,看看这次我需要哪些特定的依赖项。
它带给我另外一条路:每当我只需要某个服务的一种方法时,我必须满足它的大部份依赖项,即使我实际上并不需要它。 当然,它是由 DI 框架处理的,但我仍然必须去注册大部份那些框架。 通常这可能是一个问题,如果其中一些依赖项在另一个程序集中声明,现在他们必须引用它。 在某些情况下,它会搞砸他们的架构,所以现在他们不得已搞乱继承或将一些代码移到一个单独的服务中,从而增加他们系统中的组件数量。 当然可行,但仍然很不愉快。
在表达式世界中,它以不同的方式完成。 这里最酷的家伙是纯表达式,而不是对象。 大多数情况下,他们处理的是不可变值,而不是可变变量。 此外,表达式很容易组合,所以在大多数情况下他们甚至根本不需要
一个简单的案例可能是这样的:
let getReport queryData =use connection = getConnection() queryData |> DataRepository.get connection // 连接依赖被注入到表达式中,而不是在构造表达式中// 现在他们不需要继续跟踪依赖的生命周期 |> Report.build
对于那些不熟悉 |> 运算符和柯里化的人来说,这等同于以下代码:
let gerReport queryData =use connection = getConnection() Report.build(DataRepository.get connection queryData)
在 C#代码中:
public ReportModel GetReport(QueryData queryData){ using(var connection = GetConnection()) { // 这里的Report是一个动态类。return Report.Build(DataRepository.Get(connection, queryData)); }}
由于表达式很容易组合,他们能这样做:
let getReport queryData =use connection = getConnection() queryData |> (DataRepository.get connection >> Report.build)
现在请注意,能更轻松地测试 Report.build。 你根本不需要模拟。 不仅如此,还有一个框架 FsCheck,它能生成数百个输入参数并采用它运行测试,向您显示哪些参数破坏了它。 现在这是一种适度的测试,即使这样的测试确实会试图将您的系统钉在十字架上,而单元测试更像是轻轻地挠痒痒。
要运行那些测试,您所要做的就是为您的类别定义一个生成器。 为何它比模拟更好? 即使生成器是通用的,它适用于你未来的大部份测试,你不需要知道任何人实现来创建它。
顺便说一句,您的业务逻辑程序集没引用带有存储库的程序集,也没引用带有它接口的程序集。 这意味着如果您想从 EntityFramework 切换到 Dapper,您的 BL 程序集将不会受到任何人影响。
动态解析类别参数
展示比讲述更好:
let inline square (x: ^a when ^a: (static member (*): ^a -> ^a -> ^a)) = x * x
此函数适用于每个类别,它具有具有令人满意的签名的乘法运算符。 它不仅适用于运算符,也适用于常用方法!
open System.Threading.Tasks
type A() =
member this.GetBodyAsync() = Task.FromResult 1type B() =
member this.GetBodyAsync() = async { return 2 }A() |> GetBodyAsync |> fun x -> x.Result // 1
B() |> GetBodyAsync |> Async.RunSynchronously // 2你不需要为它做包装袋器和接口,你只需要那些类别有正确的方法! 我不知道你如何在 C# 中做到这一点。
排序表达式
他们采用了一个 Result 类别的例子。 考虑一下,他们有一系列操作,每个操作都返回该结果。 如果其中任何人一个引致错误,他们想在那时停止执行。
而不是像这样写无尽的阶梯:
let res arg = match doJob arg with | Error e -> Error e | Ok r -> match doJob2 r with | Error e -> Error e| Ok r -> …
他们能定义一次:
type ResultBuilder() =
member __.Bind(x, f) =
match x with
| Error e -> Error e
| Ok x -> f x
member __.Return x = Ok x
member __.ReturnFrom x = x let result = ResultBuilder()像这样在任何人地方采用它:
let res arg =
result {
let! r = doJob arg
let! r2 = doJob2 r
let! r3 = doJob3 r2
return r3
}现在每一行都有 let! 在错误 e 的情况下,他们返回错误。 如果一切正常,他们返回那个 Ok r3。 您能对任何人事情执行类似的操作,包括采用自定义名称的自定义操作。 它是制作 DSL 的绝佳辅助工具。
顺便说一句,异步编程也有类似的东西,甚至有两个——task和async。 第一个用于熟悉的任务,第二个用于处理 Async 类。 这个东西来自 F#,与 task 的主要就区别在于它有一个冷启动,但它也与 Tasks API 集成。 您可以采用并行和/或级联执行构建复杂的异步工作流,并仅在它准备就绪时运行它。 像这样:
let myTask =
task {
let! result = doSmthAsync() // 就像await Task
let! result2 = doSmthElseAsync(result)
return result2
}let myAsync =
async {
let! result = doAsync()
let! result2 = do2Async(result)
do! do3Async(result2)
return result2
} let result2 = myAsync |> Async.RunSynchronouslylet result2Task = myAsync |> Async.StartAsTasklet result2FromTask = myTask |> Async.AwaitTask项目中的文件结构
由于记录和可界定联手定义非常短,而且您的项目中通常没太多其它类别,因此项目中的文件数量大大减少。 大部份域类别都能在 1 个文件中定义。
此外,在 F# 中,文件顺序和代码顺序很重要:预设情况下,您只能在给定的代码行中采用声明更高的内容。 它是通过设计完成的,非常棒,即使它能防止他们产生循环依赖。 它在代码审查期间也有很大帮助:文件顺序揭示了设计错误。 如果某个高级组件定义在该层次结构的较高位置,则说明有人搞砸了依赖关系。 你一眼就能看出,现在想象一下当你处理 C# 时需要多长时间。
总结
一旦你拥有了那些强大的辅助工具并习惯了它,你就会开始更快、更优雅地解决问题。 你的大部分代码撰写并测试一次就能永远工作。 回到 C# 意味着失去生产力。 从前我骑摩托车,现在我又骑自行车了。 我的意思是 C# 很好,但 F# 很棒。 当你拥有一个很棒的东西时,为何还需要另外一些东西,对吧? 是的,C# 也在慢慢获得其中的一些特性——可为 null 的引用类别、模式匹配,甚至可能是记录。 但是那些特性伴随着巨大的延迟,并且与 F# 相比它要弱得多。 可为空的引用类别很好,但 Option<T> 好得多,原因有几个,模式匹配没那么强大,我想记录不会有类似的语法。 而且仍然存在本体论问题,它剥夺了他们代码稳定性和基于特性的测试。 那些测试实际上在提交之前多次向我揭示了设计错误。 QA 团队需要很长时间才能找到类似的东西。
另一方面,单元测试通常会告诉我我忘记更新测试配置。 是的,有时他们会告诉我我在代码中遗漏了一些东西。 甚至无法在 F# 中编译的东西。
我会说 F# 的最大问题是很难将其介绍给 C# 合作开发人员。 但如果您试著一下,它会很容易。
