为什么类型推断有用?


37

我阅读代码的方式比编写代码的方式要多,并且我假设大多数从事工业软件工作的程序员都这样做。我认为类型推断的优点是减少了冗长和编写的代码。但是另一方面,如果您更频繁地阅读代码,则可能需要可读的代码。

编译器推断类型;有旧的算法可以解决这个问题。但是真正的问题是,当我阅读代码时,程序员为什么要推断出变量的类型?只是阅读该类型的人,是否比思考那里的类型还快?

编辑:作为结论,我理解为什么它有用。但是在语言功能类别中,我发现它在带有操作符重载的存储桶中显示-在某些情况下很有用,但如果滥用则会影响可读性。


5
以我的经验,编写代码比阅读代码要重要得多。在阅读代码时,我正在寻找算法和特定的块,这些变量和命名块通常会将我定向到。除非编写得很糟糕,否则我真的不需要键入检查代码就可以阅读和理解它的功能。但是,当阅读的代码中充斥着我不需要的多余不必要的细节(例如太多的类型注释)时,通常会使查找我所寻找的位变得更加困难。我想说类型推断对阅读远胜于编写代码,这是一个巨大的福音。
Jimmy Hoffa 2014年

一旦找到所需的代码,就可以开始对其进行类型检查,但是在任何给定的时间,您都不应专注于十行以上的代码,这时不必麻烦进行自我推断是因为您一开始就在头脑中将整个区块分开,并且很可能会使用工具来帮助您做到这一点。弄清楚要尝试划分的10行代码的类型,几乎不会花费您太多时间,但这是您从阅读切换到编写的部分,无论如何这是很少见的。
Jimmy Hoffa 2014年

请记住,即使程序员读取代码的时间多于编写代码的时间,但这并不意味着一段代码的读取次数比编写的频率高。许多代码可能是短暂的,否则就永远不会再读取,并且很难分辨哪个代码将继续存在并应以最大的可读性编写。
jpa 2014年

2
详细介绍@JimmyHoffa的第一点,请考虑阅读一般内容。当专注于单个单词的词性时,句子是否更易于解析和阅读,更不用说理解了?“((名词)牛(名词奇异)跳过(动词过去)(介词)((名词)月亮(名词)。(标点符号)”。
Zev Spitz

Answers:


46

让我们看一下Java。Java不能包含具有推断类型的变量。这意味着我经常要拼写出该类型,即使对于人类读者来说,类型是什么也很明显:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

有时拼写整个类型只是很烦人。

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

这种冗长的静态类型妨碍了我,程序员。大多数类型注释都是重复的行填充符,是对我们已经知道的无内容限制。但是,我确实喜欢静态类型,因为它确实可以帮助发现错误,因此使用动态类型并不总是一个好的答案。类型推断是两全其美的方法:我可以省略不相关的类型,但仍要确保我的程序(类型-)已签出。

虽然类型推断对于局部变量确实很有用,但不应将其用于必须明确记录的公共API。有时,类型对于了解代码中发生的事情确实至关重要。在这种情况下,仅依靠类型推断是愚蠢的。

有许多语言支持类型推断。例如:

  • C ++。该auto关键字触发类型推断。没有它,拼写出lambda的类型或容器中条目的类型将是地狱。

  • C#。您可以使用声明变量var,这会触发有限类型的类型推断。它仍然可以处理大多数需要类型推断的情况。在某些地方,您可以完全忽略类型(例如,在lambda中)。

  • Haskell,以及ML系列中的任何语言。尽管此处使用的类型推断的特定功能非常强大,但您仍然经常会看到函数的类型注释,原因有两个:第一个是文档,第二个是检查类型推断是否实际找到了您期望的类型。如果存在差异,则可能存在某种错误。


13
还要注意,C#具有匿名类型,即没有名称的类型,但是C#具有名义上的类型系统,即基于名称的类型系统。没有类型推断,这些类型将永远无法使用!
约尔格W¯¯米塔格

10
我认为有些例子有些人为。初始化为42并不自动意味着变量是an int,它可以是任何数字类型,包括even char。我也看不出为什么Entry只想输入类名并让您的IDE进行必要的导入时为什么要拼出整个类型。唯一需要拼写全名的情况是,您在自己的程序包中有一个具有相同名称的类。但是在我看来,无论如何,它还是糟糕的设计。
马尔科姆2014年

10
@Malcolm是的,我所有的示例都是人为的。它们用来说明一个观点。在编写int示例时,我正在考虑大多数具有类型推断功能的语言(在我看来,这是相当理智的行为)。他们通常会推断出该语言中的intInteger或其他名称。类型推断的优点在于它总是可选的。您仍然可以根据需要指定其他类型。关于Entry示例:好点,我将替换为Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>。Java甚至没有类型别名:(
amon

4
@ m3th0dman如果类型对于理解很重要,那么您仍然可以明确提及它。类型推断始终是可选的。但是在这里,的类型colKey既明显又无关紧要:我们只关心它适合作为的第二个参数doSomethingWith。如果要将该循环提取到产生Iterable (key1, key2, value)-triples 的函数中,则最通用的签名将是<K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table)。在该函数内部,colKeyInteger,not K2)的实类型绝对无关紧要。
阿蒙2014年

4
@ m3th0dman这是一个广泛的声明,关于“ 大多数 ”代码是这样或那样。轶事统计。在初始化器中两次编写类型肯定没有意义View.OnClickListener listener = new View.OnClickListener()。即使程序员“懒惰”并将其缩短为var listener = new View.OnClickListener(如果可能的话),您仍然会知道类型。这种冗余是常见的-我不会在这里风险瞎猜-将其取出从思考未来的读者干。每种语言功能都应谨慎使用,我并不是在问这个问题。
Konrad Morawski 2014年

26

的确,读取代码的频率远远高于编写代码的频率。但是,阅读也要花费时间,并且两个屏幕的代码比一个屏幕的代码难于导航和阅读,因此我们需要优先考虑打包最佳的有用信息/阅读努力比率。这是一般的UX原理:过多的信息会立即淹没并实际上降低了接口的有效性。

它是我的经验,通常,精确的类型(是)非常重要的。当然,你有时窝表达式:x + y * zmonkey.eat(bananas.get(i))factory.makeCar().drive()。每个子表达式都包含子表达式,这些子表达式求值的类型不会被写出。但是,它们非常清楚。我们可以保留该类型为未声明状态,因为它很容易从上下文中找出来,而写出来的弊大于利(弄乱数据流的理解,占用宝贵的屏幕和短期存储空间)。

不像没有明天那样嵌套表达式的原因之一是行变长并且值的流变得不清楚。引入临时变量对此有帮助,它强加了顺序并为部分结果命名。但是,并非所有从这些方面受益的事物都可以从其类型的详细说明中受益:

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

不要紧,无论user是实体对象,整数,字符串或其他什么东西?在大多数情况下,它不是全部,仅知道它代表用户,就来自HTTP请求,就可以用来获取名称以显示在答案的右下角。

而且,当它确实重要时,作者可以自由地写出类型。这是必须负责任地使用的自由,但是对于其他可以提高可读性的其他事物(变量和函数名称,格式,API设计,空白)也是如此。确实,Haskell和ML中的约定(无需费力即可推断出所有内容)是写出非局部函数的类型,以及适当时写出局部变量和函数的类型。只有新手才能推断出每种类型。


2
+1这应该是公认的答案。这恰恰是为什么类型推断是一个好主意的核心。
Christian Hayter 2014年

user如果您要扩展功能,确切的类型确实很重要,因为它决定了您可以使用做什么user。如果您想添加一些完整性检查(例如,由于一个安全漏洞),或者忘记除了显示它,实际上还需要对用户执行某些操作,则这一点很重要。的确,这类扩展阅读不仅只是阅读代码而已,但它们也是我们工作的重要组成部分。
cmaster

@cmaster而且您总是可以很容易地找到该类型(大多数IDE会告诉您,并且存在一种技术含量低的解决方案,有意造成类型错误并让编译器打印实际的类型),这是很麻烦的在通常情况下不会惹恼您。

4

我认为类型推断非常重要,任何现代语言都应支持。我们所有人都在IDE中进行开发,如果您想了解推断的类型,它们可能会对您有很大帮助,只有极少数人会参与进来vi。例如,考虑一下Java中的冗长和仪式代码。

  Map<String,HashMap<String,String>> map = getMap();

但是您可以说我的IDE可以帮助我很好,这可能是正确的一点。但是,如果没有类型推断(例如C#匿名类型)的帮助,某些功能将不存在。

 var person = new {Name="John Smith", Age = 105};

LINQ不会像你一样,现在是没有类型推断的帮助下,Select例如

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

该匿名类型将巧妙地推导给变量。

我不喜欢对返回类型进行类型推断,Scala因为我认为您的观点适用于此,我们应该清楚函数返回的内容,以便我们可以更流畅地使用API


Map<String,HashMap<String,String>>?当然,如果您不使用类型,则将它们拼写几乎没有什么好处。Table<User, File, String>虽然信息量更大,但是编写它有好处。
MikeFHay 2014年

4

我认为答案很简单:它节省了读写冗余信息的时间。特别是在面向对象的语言中,等号两边都有类型。

这还会告诉您何时应该使用或不应该使用它-何时信息不是多余的。


3
嗯,从技术上讲,当可以省略手动签名时,信息总是多余的:否则编译器将无法推断它们!但是,我的意思是:当您在一个视图中将签名复制到多个位置时,对大脑来说确实是多余的,而一些布局合理的类型可以提供您需要长时间搜索的信息,可能需要使用许多非显而易见的转换。
左右左转

@leftaroundabout:程序员读取时多余。
jmoreno 2014年

3

假设有人看到代码:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

如果someBigLongGenericType可以从的返回类型中分配,则someFactoryMethod阅读代码的人有多大可能会注意到类型是否不完全匹配,并且注意到差异的人将如何容易地识别出差异是否是有意的?

通过允许推论,一种语言可以向正在阅读代码的人建议,当明确声明变量的类型时,该人应尝试找到它的原因。反过来,这使正在阅读代码的人员可以更好地集中精力。相比之下,如果在大多数情况下指定类型时,它恰好与所推断的类型完全相同,那么正在阅读代码的人可能不太会注意到它与众不同的时间。


2

我看到已经有很多好的答案。我将重复其中的一些,但有时您只想用自己的话说。我将用C ++的一些示例进行评论,因为这是我最熟悉的语言。

必要的事情永远都不是不明智的。类型推断对于使其他语言功能切实可行是必需的。在C ++中,可能有无法说明的类型。

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C ++ 11添加了同样难以言喻的lambda。

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

类型推断也支持模板。

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

但是您的问题是“为什么我(程序员)为什么在阅读代码时要推断变量的类型?对于每个人来说,阅读类型是否比思考其中的类型更快?”

类型推断可消除冗余。在阅读代码时,有时在代码中包含冗余信息可能会更快,更容易,但是冗余会掩盖有用的信息。例如:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

C ++程序员不需要很熟悉标准库就可以确定i是迭代器,i = v.begin()因此显式类型声明的价值有限。通过它的存在,它会遮盖更重要的细节(例如,i指向矢量的开头)。@amon的很好答案提供了一个更好的例子,详细程度使重要细节难以理解。相比之下,使用类型推断则使重要细节更加突出。

std::vector<int> v;
auto i = v.begin();

尽管阅读代码很重要,但这还不够,但是在某些时候,您将不得不停止阅读并开始编写新代码。代码冗余使修改代码变得越来越慢。例如,说我有以下代码片段:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

在我需要更改向量的值类型的情况下,将代码加倍更改为:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

在这种情况下,我必须在两个地方修改代码。与类型推断相反,原始代码为:

std::vector<int> v;
auto i = v.begin();

以及修改后的代码:

std::vector<double> v;
auto i = v.begin();

请注意,我现在只需要更改一行代码。将其推断到大型程序中,类型推断可以比使用编辑器更快地将更改传播到类型。

代码冗余会产生错误。每当您的代码依赖于两条信息保持相等时,就有可能出错。例如,此语句中的两种类型之间存在不一致,这可能不是故意的:

int pi = 3.14159;

冗余使意图难以辨别。在某些情况下,类型推断可以比显式类型规范更容易阅读和理解。考虑一下代码片段:

int y = sq(x);

sq(x)返回an 的情况下,int是否y为an 并不明显,int因为它是的返回类型sq(x)或因为它适合使用的语句y。如果我更改其他代码以使其sq(x)不再返回int,则仅从该行就不能确定是否y应更新的类型。与相同的代码对比,但使用类型推断:

auto y = sq(x);

在这种情况下,意图很明确,y必须与所返回的类型相同sq(x)。当代码更改的返回类型时sq(x)y更改的类型将自动匹配。

在C ++中,还有第二个原因,上面的示例使用类型推断更简单,类型推断不能引入隐式类型转换。如果返回类型sq(x)不是int,则编译器会静默插入到的隐式转换int。如果的返回类型sq(x)是定义的类型复杂类型operator int(),则此隐藏函数调用可能是任意复杂的。


关于C ++中不可言喻的类型的一个很好的说法。但是,我认为这不是添加类型推断的原因,而是修复语言的原因。在您遇到的第一种情况下,程序员只需要给事物起一个名称即可避免使用类型推断,因此这不是一个强力的例子。第二个例子很强大,因为C ++明确禁止lambda类型是可发音的,即使使用typeof语言的typedef 也使该语言无法使用。在我看来,这是语言本身的不足之处。
cmaster
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.