为什么说你该学学F#?(二)

翻译:顾远山
版权归作者所有,转载请标明出处。
原文链接:Why you should learn F#
原文作者:Dustin Moris Gorski
原贴较长,遂翻译时将其拆分成若干篇并组成序列,此篇为系列之二。


不可变性和没有空值

F#最棒的特性之一是它默认对象不可变且不可为空,这使得推理代码和实现无错应用程序变得容易很多。无需考虑某对象被传入函数后会否改变状态,或同样无需检查空值,码农们藉此能极大提高的应用交付的效率和质量。

和空值说再见

空值引用的发明者托尼霍尔(当然他还发明了很多其他伟大的东西),把它称为他犯下的十亿美元的错误,他甚至在2009年伦敦举办的QCon大会中为创造了null而道歉。

null的问题在于它并不反映任何真实状态却同时具有太多含义。你永远搞不明白null到底代表什么,是“未知”、或是“空”、或是“不存在”、或是“未被初始化”、或是“无效”、或是“其他错误”,还是“行/文件/流的结尾等”?今时今日学者们一致认为null的存在已然是个错误,这就是为什么很多语言比如C#尝试逐渐把它从后续版本中剔除掉的原因。

所幸的是F#从一开始就没有空值。如果你要强行在F#中使用null,那只有一种场景——就是跟C#互操作,这里并没有完全隔离。

不可变性 > 可变性

可变性是函数式编程真正发挥作用的另一个主题。问题不在于可变性本身,而在于在某门编程语言中默认情况下对象是否可变。可变性与不可变性两者互斥,而每一门编程语言都必须只选它想要的那一个。

不可变性有个好处,那就是让代码变得更容易理解,同时它也能防止很多错误,因为某个对象被创建后,就没有类、没有方法或函数能改变它的状态。这一点特别有用,尤其是这个对象需要在很多不同的方法之间传来传去,而且我们不清楚那些方法的内部实现(如第三方代码等)的时候。

相反,可变性并没有带来太多好处。它让代码变得更难理解,引入更多的方式让类和方法超越各自的职责,还让写得很烂的库带来意想不到的行为。可变性能让代码直接修改对象状态,带来的好处很少,却要付出更高的代价。

现在问题是哪个更容易:在默认不可变性的编程语言中改变一个对象?还是在默认可变性的编程语言中引入不可变性?

其实只要看一眼F#如何处理改变对象状态的需求就能快速回答第一个问题,在F#中所谓的“改变”实则通过以修改后的值创建的新对象来实现:

let c = { Name = "Susan Doe"; Address = "1 Street, London, UK" }
let c' = { c with Address = "3 Avenue, Oxford, UK" }

这是个非常优雅的解决方案,它可以实现几乎和可变性相同的结果(却没有成本)。

但在C#中引入不可变性就有一点小尴尬了。

C#并没有现成的语言结构让码农创建不可变的对象。首先我得创建一个新类型,但我又不能使用类,因为类是引用类型,有可能为null,如果某个对象在创建后可以被赋值为null,那它不能是不可变的。

public class Customer
{
    public string Name { get; set; }
    public string Address { get; set; }
}
// Somewhere later in the program:
var c = new Customer();
c = null;

这种情况逼我改用结构体struct

public struct Customer
{
    public string Name { get; set; }
    public string Address { get; set; }
}

现在对于Customer这个类型来说,null已经不再是问题,但它里面的属性依然有问题:

var c = new Customer();
c.Name = "Haha gotcha!";

我们只好把setter属性访问器改为私有了:

public struct Customer
{
    public string Name { get; private set; }
    public string Address { get; private set; }
}

好一点了,但还没有实现不可变性,依旧有人能搞事情,如下:

public struct Customer
{
    public string Name { get; private set; }
    public string Address { get; private set; }
    public void ChangeName(string name)
    {
        Name = name;
    }
}
var c = new Customer();
c.ChangeName("Haha gotcha!");

问题并非ChangeName方法是公共的,而是存在方法可以在某个属性创建后依然能修改它的状态。

我们还是为这两个属性引入相应的后端字段吧,然后再把setter属性访问器去掉:

public struct Customer
{
    private string _name;
    private string _address;
    public string Name { get { return _name; } }
    public string Address { get { return _address; } }
    public Customer(string name, string address)
    {
        _name = name;
        _address = address;
    }
}

这看起来也许好起来了,但事实也没有。在这个类的内部还是有成员能修改_name_address。通过给这俩字段加上readonly可以解决问题:

public struct Customer
{
    private readonly string _name;
    private readonly string _address;
    public string Name { get { return _name; } }
    public string Address { get { return _address; } }
    public Customer(string name, string address)
    {
        _name = name;
        _address = address;
    }
}

这下Customer类型总算不可变了(至少现在是的),但代码太冗长了,这里我们能把里面两个属性折叠成public readonly字段。

public struct Customer
{
    public readonly string Name;
    public readonly string Address;
    public Customer(string name, string address)
    {
        Name = name;
        Address = address;
    }
}

或者在C# 6 (及后续版本) 我们也可以创建只读属性,如下:

public struct Customer
{
    public string Name { get; }
    public string Address { get; }
    public Customer(string name, string address)
    {
        Name = name;
        Address = address;
    }
}

到此一切还算顺利,但这要求码农得非常了解C#,不然就很容易出错。

不幸的是,现实世界的应用程序从未如此简单。

要是所谓的Customer类型长这样怎么办?

public class Address
{
    public string Street { get; set; }
}
public struct Customer
{
    public readonly string Name;
    public readonly Address Address;
    public Customer(string name, Address address)
    {
        Name = name;
        Address = address;
    }
}
var address = new Address { Street = "Springfield Road" };
var c = new Customer("Susan", address);
address.Street = "Gotcha";

通过这点我们显而易见,想在C#中引入不可变性,并不像某些人所想的那么简单。

这又是一个很好的例子展示了F#和C#之间鲜明的对比。编写正确的代码本没有那么难,而编程语言的选择真的可以带来巨大的区别。


译者注:划重点!这一小节对F#语言默认的对象不可变性展开了解释,并通过例子和C#进行对比。原文作者花了大部分笔墨阐述在默认对象可变的编程语言里实现不可变性的复杂程度,可以说是非常煞费苦心了。译者以为,其实还可以举更接地气的例子,说明对象的不可变性能带来的好处不仅能减低代码复杂程度,更重要的是对象不变能防止很多错误。比如多线程下操作对象,主流默认对象可变的编程语言如C#中的多线程编程要考虑各种锁的机制看似很高深,实际这些机制在对象生来就不可变的编程语言如F#面前显得特别笨拙,同样涉及多线程编程的话,用F#比用C#实现简单得多且不易出错。

猜你喜欢

转载自blog.csdn.net/weixin_33922672/article/details/87207284