不变类型的缺点是什么?


12

当不希望更改类的实例时,我看到自己使用越来越多的不可变类型。它需要做更多的工作(请参见下面的示例),但是可以更轻松地在多线程环境中使用这些类型。

同时,即使可变性不会使任何人受益,我也很少在其他应用程序中看到不可变类型。

问题:为什么在其他应用程序中很少使用不可变类型?

  • 这是因为编写不可变类型的代码时间更长,
  • 还是我错过了某些东西,使用不可变类型时有一些重要的缺点?

现实生活中的例子

假设您是Weather从RESTful API 获得的:

public Weather FindWeather(string city)
{
    // TODO: Load the JSON response from the RESTful API and translate it into an instance
    // of the Weather class.
}

我们通常会看到的是(删除新行和注释以缩短代码):

public sealed class Weather
{
    public City CorrespondingCity { get; set; }
    public SkyState Sky { get; set; } // Example: SkyState.Clouds, SkyState.HeavySnow, etc.
    public int PrecipitationRisk { get; set; }
    public int Temperature { get; set; }
}

另一方面,考虑到Weather从API 中获取一个,然后对其进行修改,我将以这种方式编写:更改TemperatureSky不会更改现实世界中的天气,并且更改CorrespondingCity也没有任何意义。

public sealed class Weather
{
    private readonly City correspondingCity;
    private readonly SkyState sky;
    private readonly int precipitationRisk;
    private readonly int temperature;

    public Weather(City correspondingCity, SkyState sky, int precipitationRisk,
        int temperature)
    {
        this.correspondingCity = correspondingCity;
        this.sky = sky;
        this.precipitationRisk = precipitationRisk;
        this.temperature = temperature;
    }

    public City CorrespondingCity { get { return this.correspondingCity; } }
    public SkyState Sky { get { return this.sky; } }
    public int PrecipitationRisk { get { return this.precipitationRisk; } }
    public int Temperature { get { return this.temperature; } }
}

3
“这需要更多的工作” –需要引用。以我的经验,它需要较少的工作。
Konrad Rudolph 2014年

1
@KonradRudolph:通过更多的工作,我的意思是要编写更多代码来创建一个不可变的类。我的问题中的示例对此进行了说明,对于可变类,有7行,对于不可变类,有19行。
2014年

您可以使用Visual Studio中的“代码片段”功能来减少代码键入。您可以创建您的自定义代码片段,并让IDE用几个键同时定义您的字段和属性。不可变类型对于多线程至关重要,并且在诸如Scala之类的语言中得到了广泛使用。
Mert Akcakaya

@Mert:代码段非常适合简单的事情。编写代码片段将构建一个完整的类,其中包含字段和属性的注释以及正确的顺序,这并非易事。
2014年

5
我不同意给出的示例,不变版本正在做更多和不同的事情。您可以通过使用访问器声明属性来删除实例级变量{get; private set;},甚至可变对象都应具有构造函数,因为所有这些字段都应始终设置,为什么不强制执行呢?进行这两个完全合理的更改会将它们带入特征和LoC奇偶校验。
Phoshi 2014年

Answers:


16

我使用C#和Objective-C进行编程。我真的很喜欢不变类型,但是在现实生活中,由于以下原因,我一直被迫限制其使用,主要是针对数据类型:

  1. 与可变类型相比的实现工作量。对于不可变类型,您将需要一个构造函数,该构造函数的所有属性都需要参数。您的例子是一个很好的例子。尝试想象您有10个类,每个类具有5-10个属性。为了方便起见,您可能需要有一个生成器类以类似的方式来构建或创建修改不可改变的情况下,StringBuilderUriBuilder在C#中,或WeatherBuilder在您的案件。这是我的主要原因,因为我设计的许多课程都不值得这样做。
  2. 消费者可用性。与可变类型相比,不可变类型更难使用。实例化需要初始化所有值。不变性还意味着不使用构建器便无法将实例传递给方法来修改其值,如果需要使用构建器,那么缺点就在于我的(1)。
  3. 与语言框架的兼容性。许多数据框架需要可变类型和默认构造函数才能运行。例如,您不能使用不可变类型进行嵌套LINQ-to-SQL查询,也不能绑定要在编辑器(如Windows窗体的TextBox)中编辑的属性。

简而言之,不变性对于行为类似于值或仅具有少量属性的对象是有益的。在使任何东西不变之前,您必须考虑使类成为不变之后所需的努力和类本身的可用性。


11
“实例化事先需要所有值。”:可变类型也需要,除非您接受使对象包含未初始化字段浮动的风险……
Giorgio 2013年

@Giorgio对于可变类型,默认构造函数应将实例初始化为默认状态,并且实例化的状态可以在以后更改。
2013年

8
对于不可变类型,您可以具有相同的默认构造函数,并稍后使用另一个构造函数进行复制。如果默认值对可变类型有效,则它们对不可变类型也应有效,因为在两种情况下,您都在对同一实体建模。或有什么区别?
Giorgio

1
要考虑的另一件事是类型代表什么。由于所有这些因素,数据协定并不能构成良好的不可变类型,但是使用依赖项进行初始化或仅读取数据然后执行操作的服务类型对于不可变性非常有用,因为这些操作将一致地执行并且服务状态不能为改变来冒险。
凯文(Kevin)

1
我目前使用F#编写代码,其中不可变性是默认设置(因此更易于实现)。我发现您的第3点是最大的障碍。一旦使用了大多数标准的.Net库,您将需要跳过箍。(如果他们使用反射并因此绕过了不变性……哎!)
古兰经

5

通常,使用不围绕不变性的语言创建的不可变类型通常会花费更多的开发人员时间来创建以及潜在地使用,如果它们需要某种“生成器”类型的对象来表达所需的更改(这并不意味着总的来说)工作量会更多,但在这些情况下,这需要预先付费)。同样,不管该语言是否使创建不可变类型变得非常容易,对于非平凡的数据类型,它总是总是需要一定的处理和内存开销。

使功能没有副作用

如果您使用的语言不是围绕不变性,那么我认为实用的方法不是要使每个数据类型都是不变的。可能给您带来许多相同好处的更有生产力的思维方式是,专注于最大程度地增加系统中引起零副作用的功能的数量。

举一个简单的例子,如果您有一个会引起如下副作用的函数:

// Make 'x' the absolute value of itself.
void make_abs(int& x);

然后,我们就不需要一个不可变的整数数据类型,该数据类型禁止像初始化后赋值这样的运算符使该函数避免副作用。我们可以简单地做到这一点:

// Returns the absolute value of 'x'.
int abs(int x);

现在,该函数不会造成混乱x或超出其范围,在这种琐碎的情况下,我们甚至可以避免与间接寻址/混淆相关的任何开销,从而节省了一些周期。至少第二个版本不应比第一个版本在计算上昂贵。

全部复制昂贵的东西

当然,如果要避免使函数引起副作用,大多数情况并不是那么简单。一个复杂的实际用例可能更像这样:

// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);

到那时,网格可能需要数百兆的内存,包含十万多个多边形,甚至更多的顶点和边缘,多个纹理贴图,变形目标等。复制整个网格以使其变得真正昂贵。transform功能无副作用,如下所示:

// Returns a new version of the mesh whose vertices been 
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

在这些情况下,完整复制某些内容通常是史诗般的开销,我发现将其Mesh转换为持久性数据结构和具有类比“构建器”的不可变类型以创建其修改版本非常有用,可以简单地复制和引用非唯一的零件。所有这些的重点是能够编写没有副作用的网格函数。

持久数据结构

在这些情况下,复制所有内容的成本是如此之高,我发现Mesh即使不花很多钱就可以设计出一个不可变的东西来真正获得回报,因为它不仅简化了线程安全性。它还简化了非破坏性编辑(允许用户在不修改其原始副本的情况下对网格操作进行分层),撤消系统(现在撤消系统仅可以在操作进行更改之前存储网格的不变副本,而不会消耗内存)使用)和异常安全性(现在,如果上述函数中发生异常,则该函数不必回滚并撤消其所有副作用,因为它不会导致任何副作用)。

在这些情况下,我可以自信地说,使这些庞大的数据结构不可变所需的时间比其节省的时间要多,因为我将这些新设计的维护成本与以前的设计进行了比较,而后者围绕着可变性和功能而产生了副作用,以前的可变设计花费了更多的时间,并且更容易发生人为错误,尤其是在真正引起开发人员在关键时刻忽略的区域,例如异常安全。

因此,我确实认为在这些情况下不可变数据类型确实会奏效,但是为了使系统中的大多数功能不受副作用的影响,并非必须使所有内容不变。许多东西便宜到足以完全复制。同样,许多现实世界中的应用程序将需要在这里和那里引起一些副作用(至少像保存文件一样),但是通常会有更多的功能避免副作用。

对我来说,拥有一些不可变的数据类型的目的是确保我们可以编写最大数量的函数而不会产生副作用,而不会产生史诗般的开销,其形式是当只有一小部分时左右全部复制深度数据结构其中的一些需要修改。在这种情况下,拥有持久的数据结构将最终成为优化细节,使我们能够编写没有副作用的函数,而无需为此付出大笔的费用。

不变的开销

现在,从概念上讲,可变版本将始终在效率方面占优势。始终存在与不变数据结构相关的计算开销。但是在上述情况下,我发现这是值得进行的交换,您可以专注于使开销在本质上足够小。我更喜欢正确性容易且优化变得困难的方法,而不是优化容易但正确性变得更难的方法。使代码能够完美地正常运行并没有什么令人沮丧的事情,无论它们以多快的速度实现不正确的结果,首先都需要对那些不能首先正常运行的代码进行更多的调整。


3

我能想到的唯一缺点是,从理论上讲,使用不可变数据可能比使用可变数据慢—创建新实例并收集先前实例比修改现有实例要慢。

另一个“问题”是您不能只使用不可变类型。最后,您必须描述状态,并且必须使用可变类型来描述状态-不更改状态就无法做任何工作。

但是,仍然普遍的规则是,只要有理由,就尽可能使用不可变类型,并使类型可变。

并回答“ 为什么不可变类型很少在其他应用程序中使用? ”这个问题-我真的不认为它们是...无论您在哪里看,每个人都建议使类尽可能地不变。例如:http://www.javapractices.com/topic/TopicAction.do?Id=29


1
但是,您的两个问题都不在Haskell中。
Florian Margaine 2013年

@FlorianMargaine您能详细说明吗?
mrpyo

得益于智能编译器,这种慢速是不正确的。在Haskell中,甚至I / O也通过不可变的API进行。
Florian Margaine 2013年

2
比速度更根本的问题是,不可变对象在状态改变时很难保持身份。如果使用Car特定的物理汽车的位置不断更新易变的对象,那么如果我引用该对象,则可以快速,轻松地找到该汽车的下落。如果Car是一成不变的,那么找到当前下落可能会困难得多。
超级猫

有时,您必须非常聪明地编写代码,以使编译器确定没有对遗留的先前对象的引用,因此可以对其进行修改或进行森林砍伐转换等。特别是在较大的程序中。正如@supercat所说,身份确实会成为一个问题。
Macke 2014年

0

要对事物可能发生变化的任何现实世界系统进行建模,就需要以某种方式对可变状态进行编码。对象可以保持可变状态的主要方式有三种:

  • 使用对不可变对象的可变引用
  • 使用对可变对象的不变引用
  • 使用对可变对象的可变引用

使用first使对象很容易制作当前状态的不可变快照。使用第二个对象使对象易于创建当前状态的实时视图。在预期几乎不需要不变的快照或实时视图的情况下,使用第三种方法有时可以使某些操作更有效率。

除了使用对不可变对象的可变引用存储的状态更新通常比使用可变对象存储的状态更新慢之外,使用可变引用将要求人们放弃构造状态廉价实时视图的可能性。如果不需要创建实时取景,这不是问题。但是,如果需要创建实时视图,则无法使用不可变的引用将对该视图进行所有操作- 包括读取和写入-比原本要慢得多。如果对不可变快照的需求超出了对实时视图的需求,则不可变快照的性能提高可能证明对实时视图的性能造成了影响,但是如果需要实时视图而不需要快照,则使用对可变对象的不可变引用是一种方法。去。


0

在您的情况下,答案主要是因为C#对不可变性的支持不佳...

如果满足以下条件,那就太好了:

  • 除非另有说明(例如,使用“ mutable”关键字),否则默认情况下所有内容都是不可变的,将不可变和可变类型混合使用会造成混淆

  • 变异的方法(With)会自动可用-尽管这已经可以实现看随着

  • 有一种方法可以说特定方法调用的结果(即ImmutableList<T>.Add)不能被丢弃,或者至少会产生警告

  • 并且主要是如果编译器可以在请求时尽可能确保不变性(请参阅https://github.com/dotnet/roslyn/issues/159


1
第三点,ReSharper具有一个MustUseReturnValueAttribute自定义属性,正是该属性完成了此任务。PureAttribute具有相同的效果,甚至更好。
塞巴斯蒂安·雷德尔

-1

为什么不可变类型在其他应用程序中很少使用?

无知?没有经验?

不可变对象在当今被广泛认为是优越的,但这是相对较新的发展。那些没有及时了解最新信息,或者只是停留在“他们所知道的信息”上的工程师将不会使用它们。为了有效使用它们,确实需要进行一些设计更改。如果应用程序很旧,或者工程师的设计技能很弱,那么使用它们可能会很尴尬或麻烦。


“尚未掌握最新技术的工程师”:可以说工程师还应该学习非主流技术。不变性的想法直到最近才成为主流,但这是一个非常古老的想法,并且由Scheme,SML,Haskell等较旧的语言支持(如果未强制执行)。因此,任何习惯于超越主流语言的人都可以在30年前了解它。
Giorgio

@Giorgio:在某些国家/地区,许多工程师仍在编写没有LINQ,没有FP,没有迭代器和没有泛型的C#代码,因此实际上,他们某种程度上错过了自2003年以来C#发生的一切。如果他们甚至不了解自己的偏好语言,我很难想象他们会知道任何非主流语言。
2013年

@MainMa:很好,您用斜体写了工程师一词。
乔治,

@乔治:在我国,他们也被称为建筑师顾问和许多其他虚荣的用语,从来没有用斜体字写过。在我目前工作的公司中,我被称为分析师开发人员,预计我将花时间为糟糕的旧版HTML代码编写CSS。职称在许多层面上都令人不安。
Arseni Mourzenko

1
@MainMa:我同意。像工程师或顾问这样的头衔通常只是流行语,没有被广泛接受的含义。他们通常使某人或其职位比实际更重要/更有声望。
Giorgio
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.