以动态类型的语言从单个函数返回不同的数据类型不是一个好主意吗?


65

我的主要语言是静态类型(Java)。在Java中,您必须从每个方法返回单个类型。例如,您不能使用有条件返回a String或有条件返回a的方法Integer。但是例如在JavaScript中,这是很有可能的。

用静态类型的语言,我明白了为什么这是一个坏主意。如果返回Object了每个方法(所有类都继承自该方法的公共父级),那么您和编译器将不知道您在处理什么。您必须在运行时发现所有错误。

但是在动态类型语言中,甚至可能没有编译器。在动态类型的语言中,对于我来说,为什么返回多个类型的函数不是一个好主意并不为人所知。我在静态语言方面的背景使我避免编写此类函数,但是我担心自己会紧盯着一种功能,该功能可以使我的代码更清晰一些,从而无法看到。


编辑:我将删除我的示例(直到我可以想到一个更好的示例)。我认为这引导人们回答我不想提出的观点。


为什么不在错误情况下引发异常?
TrueWill

1
@TrueWill我会在下一句话中解决这个问题。
Daniel Kaplan 2014年

您是否注意到这是动态语言中的常见做法?它在函数式语言中很常见,但是其中的规则非常明确。而且我知道,尤其是在JavaScript中,允许参数为不同的类型是很常见的(因为这实际上是进行函数重载的唯一方法),但是我很少见到这适用于返回值。我能想到的唯一一个例子是PowerShell,它具有到处都是自动数组包装/展开的功能,并且脚本语言有点例外。
Aaronaught

3
没有提到,但是有很多以返回类型为参数的函数示例(甚至在Java泛型中)。例如,在Common Lisp中,我们有(coerce var 'string)a string或类似的收益(concatenate 'string this that the-other-thing)。我也写过类似的东西ThingLoader.getThingById (Class<extends FindableThing> klass, long id)。而且,在那儿,我可能只会返回loader.getThingById (SubclassA.class, 14)SubclassBSubclassA
可以满足

1
动态类型化的语言就像电影《黑客帝国》中汤匙。不要尝试将Spoon定义为字符串数字。那是不可能的。相反,只是尝试了解真相。那没有汤匙
Reactgular 2014年

Answers:


42

与其他答案相反,在某些情况下,返回不同的类型是可以接受的。

例子1

sum(2, 3)  int
sum(2.1, 3.7)  float

在某些静态类型的语言中,这涉及重载,因此我们可以考虑存在几种方法,每种方法都返回预定义的固定类型。在动态语言中,这可能是相同的功能,实现为:

var sum = function (a, b) {
    return a + b;
};

功能相同,返回值的类型不同。

例子2

假设您收到来自OpenID / OAuth组件的响应。某些OpenID / OAuth提供程序可能包含更多信息,例如人的年龄。

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Facebook',
//     name: {
//         firstName: 'Hello',
//         secondName: 'World',
//     },
//     email: 'hello.world@example.com',
//     age: 27
// }

其他人将是最低要求,无论是电子邮件地址还是笔名。

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Google',
//     email: 'hello.world@example.com'
// }

同样,相同的功能,不同的结果。

在这里,返回不同类型的好处在您不关心类型和接口,但实际上包含哪些对象的情况下尤为重要。例如,让我们想象一个网站包含成熟的语言。然后findCurrent()可以像这样使用:

var user = authProvider.findCurrent();
if (user.age || 0 >= 16) {
    // The person can stand mature language.
    allowShowingContent();
} else if (user.age) {
    // OpenID/OAuth gave the age, but the person appears too young to see the content.
    showParentalAdvisoryRequestedMessage();
} else {
    // OpenID/OAuth won't tell the age of the person. Ask the user himself.
    askForAge();
}

将其重构为代码,每个提供程序都将具有其自己的函数,该函数将返回定义良好的固定类型,这不仅会降低代码库并导致代码重复,而且不会带来任何好处。一个人可能最终会做出类似的恐怖:

var age;
if (['Facebook', 'Yahoo', 'Blogger', 'LiveJournal'].contains(user.provider)) {
    age = user.age;
}

5
“在静态类型语言中,这涉及到重载” 我想您的意思是“在某些静态类型语言中” :)好的静态类型语言不需要为sum示例之类的东西重载。
Andres F.

17
为了您的具体例子,考虑哈斯克尔:sum :: Num a => [a] -> a。您可以汇总一个数字列表。与javascript不同,如果您尝试对非数字的东西求和,则在编译时会捕获该错误。
Andres F.

3
@MainMa在Scala中,Iterator[A]具有method def sum[B >: A](implicit num: Numeric[B]): B,它再次允许对任何类型的数字求和,并在编译时进行检查。
PetrPudlák2014年

3
@Bakuriu当然,您可以编写这样的函数,尽管您首先必须+为字符串重载(通过实现Num,这在设计上是一个糟糕的主意,但是合法的),或者发明一个由整数和字符串重载的其他运算符/函数分别。Haskell通过类型类具有临时多态性。既包含整数又包含字符串的列表要困难得多(如果没有语言扩展,这是不可能的),但这是另一回事。

2
你的第二个例子并没有给我特别的印象。这两个对象是相同的概念“类型”,只是在某些情况下未定义或填充某些字段。您甚至可以使用带有null值的静态类型的语言来表示它。
Aaronaught

31

通常,出于相同的原因,这是一个坏主意,因为静态类型语言中的道德等价物是一个坏主意:您不知道返回哪种具体类型,所以您不知道可以对结果做什么(除了可以用任何值完成的几件事)。在静态类型系统中,您具有返回类型等的编译器检查的注释,但在动态语言中,仍然存在相同的知识-这只是非正式的,存储在头脑和文档中,而不是源代码中。

但是,在许多情况下,返回类型的押韵和原因很明显,其效果类似于静态类型系统中的重载或参数多态。换句话说,结果类型可预测的,只是表达起来并不那么简单。

但是请注意,特定功能的设计不良可能还有其他原因:例如,sum在无效输入上返回false 的功能是一个糟糕的主意,主要是因为该返回值无用且容易出错(0 <->错误混淆)。


40
我的两分钱:我讨厌那件事发生。我见过JS库在没有结果时返回null,在一个结果上返回Object,在两个或多个结果上返回Array。因此,您不仅要遍历数组,还必须确定它是否为null,是否为null,是否为数组,是否为空,然后做一件事,否则做其他事情。特别是因为从通常的意义上讲,任何明智的开发人员都会将Object添加到Array并从处理数组中回收程序逻辑。
phyrfox 2014年

10
@Izkata:我根本不认为这NaN是“对立”。NaN实际上是任何浮点计算的有效输入。您可以做任何与NaN实际数字一样的事情。诚然,上述计算的最终结果可能不会对你非常有用,但它不会导致奇怪的运行时错误,你并不需要检查它所有的时间 -你可以只在一个结束检查一次系列计算。NaN不是一个不同的类型,它只是一个特殊的,类似于Null Object
Aaronaught

5
@Aaronaught另一方面,NaN与null对象一样,当发生错误时,它们往往会隐藏起来,仅在以后弹出。例如,如果NaN进入条件循环,您的程序可能会崩溃。
Izkata 2014年

2
@Izkata:NaN和null具有完全不同的行为。空引用将在访问后立即爆炸,NaN会传播。后者发生的原因很明显,它使您可以编写数学表达式而不必检查每个子表达式的结果。我个人认为null更具破坏性,因为NaN代表了一个正确的数字概念,没有这个数字,就无法逃脱,从数学上讲,传播是正确的行为。
Phoshi 2014年

4
我不知道为什么这变成了“空与其他所有”的论点。我只是指出,该NaN值实际上不是(a)不同的返回类型,并且(b)具有明确定义的浮点语义,因此,实际上不适合作为可变类型返回值的类似物。的确,整数算术与并没有什么不同。如果零“意外地”滑入您的计算中,那么您最终可能会以零为结果或除以零的错误。IEEE浮点定义的“无穷大”值是否也有害?
Aaronaught

26

在动态语言中,您不应该询问是否返回不同的类型,而是返回具有不同API的对象。由于大多数动态语言并不真正在乎类型,而是使用鸭子输入的各种版本。

当返回不同的类型时有意义

例如,此方法很有意义:

def load_file(file): 
    if something: 
       return ['a ', 'list', 'of', 'strings'] 
    return open(file, 'r')

因为文件和字符串列表都是(在Python中)返回字符串的可迭代项。不同的类型,相同的API(除非有人尝试调用列表中的文件方法,但这是不同的故事)。

您可以有条件地返回listtupletuple是python中的不可变列表)。

正式甚至做:

def do_something():
    if ...: 
        return None
    return something_else

要么:

function do_something(){
   if (...) return null; 
   return sth;
}

返回不同的类型,因为python None和Javascript null都是它们自己的类型。

所有这些用例都有对应的静态语言,函数只会返回适当的接口。

有条件地返回具有不同API的对象时,这是一个好主意

至于返回不同的API是否是一个好主意,在大多数情况下,IMO都没有意义。想到的唯一明智的例子就是@MainMa所说的内容:当您的API可以提供不同数量的详细信息时,在可用时返回更多详细信息可能是有意义的。


4
好答案。值得注意的是,Java /静态类型的等效项是让函数返回一个接口而不是特定的具体类型,而该接口表示API /抽象。
mikera 2014年

1
我想您正在挑剔,在Python中,列表和元组可能具有不同的“类型”,但是它们具有相同的“鸭子类型”,即它们支持相同的操作集。像这样的情况正是引入鸭类打字的原因。您也可以在Java中执行long x = getInt()。
Kaerber 2014年

“返回不同类型” 是指 “返回具有不同API的对象”。(某些语言使对象构造API的基本部分,有些则没有;这取决于类型系统的表达能力。)在您的列表/文件示例中,do_file以任何一种方式返回可迭代的字符串。
Gilles 2014年

1
在该Python示例中,您还可以同时调用iter()列表和文件,以确保在两种情况下都只能将结果用作迭代器。
RemcoGerlich 2014年

1
返回None or something是通过PyPy,Numba,Pyston等工具完成性能优化的杀手er。这是使python不太快的Python主义之一。
马特

9

您的问题使我想哭一点。不是您提供的示例用法,而是因为有人会不经意间将这种方法推广得太远了。离可笑的难以维护的代码仅一小步之遥。

错误条件用例是有意义的,并且静态类型语言中的空模式(一切都必须是模式)可以执行相同类型的操作。您的函数调用返回object或返回null

但它是一个简短的一步说:“我会用它来创建一个工厂模式 ”,并在回报foobarbaz根据功能的心情。当调用者期望foo但被告知时,调试它将成为一场噩梦bar

所以我不认为你是开诚布公的。您在使用该语言的功能时要格外谨慎。

披露:我的背景是静态类型的语言,并且我通常在较大的,多样化的团队中工作,这些团队对可维护代码的需求非常高。因此,我的观点可能也会出现偏差。


6

在Java中使用泛型允许您返回其他类型,同时仍保持静态类型的安全性。您只需在函数调用的泛型类型参数中指定要返回的类型。

当然,是否可以在Javascript中使用类似的方法是一个悬而未决的问题。由于Javascript是一种动态类型的语言,因此返回object似乎是显而易见的选择。

如果您想知道在使用静态类型语言时动态返回方案在哪里工作,请考虑查看dynamicC#中的关键字。Rob Conery能够使用关键字成功地在400行代码中编写一个对象关系映射器dynamic

当然,dynamic真正要做的只是包装object具有一定运行时类型安全性的变量。


4

我认为,有条件地返回不同的类型是一个坏主意。对于我来说,这种情况经常出现的一种方式是该函数是否可以返回一个或多个值。如果只需要返回一个值,则只返回该值而不是将其打包在Array中似乎是合理的,以避免必须在调用函数中将其拆包。但是,这(以及它的大多数其他实例)使调用者有义务区分和处理这两种类型。如果该函数始终返回相同的类型,则将更易于推理。


4

无论您的语言是否为静态键入,都存在“不良做法”。静态语言的作用更多,可以使您远离这些做法,并且您可能会发现更多的用户抱怨使用静态语言的“不良做法”,因为它是一种更正式的语言。但是,潜在的问题是用动态语言出现的,您可以确定它们是否合理。

这是您建议中令人反感的部分。如果我不知道返回什么类型,那么我将无法立即使用返回值。我必须“发现”一些有关它的东西。

total = sum_of_array([20, 30, 'q', 50])
if (type_of(total) == Boolean) {
  display_error(...)
} else {
  record_number(total)
}

通常,在代码中进行这种切换只是一种不好的做法。它使代码更难阅读。在此示例中,您将了解引发和捕获异常情况的流行原因。换句话说,如果您的函数无法执行其声明的功能,则该函数不应成功返回。如果我调用您的函数,我想这样做:

total = sum_of_array([20, 30, 'q', 50])
display_number(total)

因为第一行成功返回,所以我假设total实际包含数组的总和。如果未成功返回,我们将跳至程序的其他页面。

让我们使用另一个示例,该示例不仅仅是传播错误。也许sum_of_array会变得更聪明,并在某些情况下返回人类可读的字符串,例如“那是我的储物柜组合!” 当且仅当数组为[11,7,19]时。我很难想到一个很好的例子。无论如何,同样的问题适用。您必须先检查返回值,然后才能对其执行任何操作:

total = sum_of_array([20, 30, 40, 50])
if (type_of(total) == String) {
  write_message(total)
} else {
  record_number(total)
}

您可能会争辩说,该函数返回整数或浮点数将很有用,例如:

sum_of_array(20, 30, 40) -> int
sum_of_array(23.45, 45.67, 67.789044) -> float

但是就您而言,这些结果并不是不同的类型。您将把它们都视为数字,这就是您所关心的。因此sum_of_array返回数字类型。这就是多态性。

因此,如果您的函数可以返回多种类型,那么您可能会违反一些习惯。了解它们将帮助您确定您的特定函数是否仍应返回多种类型。


抱歉,我的可怜榜样使您误入歧途。忘了我首先提到它。您的第一段是关键。我也喜欢你的第二个例子。
丹尼尔·卡普兰

4

实际上,即使使用静态类型的语言,返回不同的类型也不是很罕见。例如,这就是为什么我们有联合类型。

实际上,Java中的方法几乎总是返回以下四种类型之一:某种对象或null异常,或者它们根本不会返回。

在许多语言中,错误条件被建模为返回结果类型或错误类型的子例程。例如,在Scala中:

def transferMoney(amount: Decimal): Either[String, Decimal]

当然,这是一个愚蠢的例子。返回类型表示“返回字符串或十进制”。按照惯例,左边的类型是错误类型(在这种情况下,是带有错误消息的字符串),右边的类型是结果类型。

这与例外类似,除了例外也是控制流构造。实际上,它们的表达能力等同于GOTO


Java中返回异常的方法听起来很奇怪。“ 返回一个例外 ” ...嗯
蚊蚋

3
异常是Java中方法的一种可能结果。实际上,我忘记了第四种类型:(Unit根本不需要返回任何方法)。没错,您实际上并没有使用return关键字,但这仍然是从方法返回的结果。对于已检查的异常,类型签名中甚至明确提到了它。
约尔格W¯¯米塔格

我懂了。您的观点似乎有一些优点,但提出的方式并没有使它看起来引人注目...恰恰相反
2014年

4
在许多语言中,错误条件被建模为返回结果类型或错误类型的子例程。异常是一个类似的想法,除了它们也是控制流的构造(GOTO实际上等于的表达能力)。
约尔格W¯¯米塔格

4

尚未提及SOLID原则。特别是,您应该遵循Liskov替换原则,即任何接受了预期类型以外的类型的类仍可以使用它们得到的任何内容,而无需进行任何测试返回的类型的操作。

因此,如果您在对象上添加了一些额外的属性,或者使用某种修饰符包装了返回的函数,而该修饰符仍然可以完成原始函数要完成的工作,那么,只要没有调用函数的代码依赖于此,行为,在任何代码路径中。

代替返回字符串或整数,一个更好的示例可能是返回洒水装置或猫。如果所有调用代码都要做的是调用functionInQuestion.hiss(),这很好。实际上,您有一个调用代码期望的隐式接口,而动态类型化的语言不会强迫您使该接口显式。

可悲的是,您的同事可能会这样做,因此您可能无论如何都要在文档中做同样的工作,除了没有一种普遍接受的,简洁的,机器可分析的方式可以做到这一点-就像在定义接口时一样有他们的语言。


3

我看到自己发送不同类型的地方是输入无效或“穷人例外”,其中“例外”条件不是很特殊。例如,从我的PHP实用程序功能库中可以看到以下简短示例:

function ensure_fields($consideration)
{
        $args = func_get_args();
        foreach ( $args as $a ) {
                if ( !is_string($a) ) {
                        return NULL;
                }
                if ( !isset($consideration[$a]) || $consideration[$a]=='' ) {
                        return FALSE;
                }
        }

        return TRUE;
}

该函数名义上返回BOOLEAN,但是在无效输入时返回NULL。请注意,自PHP 5.3起,所有内部PHP函数的行为也是如此。此外,一些内部PHP函数在标称输入上返回FALSE或INT,请参见:

strpos('Hello', 'e');  // Returns INT(1)
strpos('Hello', 'q');  // Returns BOOL(FALSE)

4
这就是为什么我讨厌PHP。好吧,无论如何,原因之一。
Aaronaught

1
不好意思是因为有人不喜欢我专业发展的语言?!?等到他们发现我是犹太人!:)
dotancohen

3
不要总是假设发表评论的人和反对投票的人是同一个人。
Aaronaught

1
我更喜欢使用异常代替null,因为(a)它很大声地失败了,所以它更有可能在开发/测试阶段得到解决,(b)它很容易处理,因为我可以一次捕获所有罕见/意外的异常我的整个应用程序(在我的前端控制器中)并记录下来(我通常会向开发团队发送电子邮件),因此可以稍后进行修复。而且我实际上讨厌标准的PHP库大部分使用“返回null”方法-实际上,这只会使PHP代码更容易出错,除非您使用进行检查,否则isset()这将带来太多负担。
scriptin

1
另外,如果返回null错误,请在docblock中将其弄清楚(例如@return boolean|null),因此,如果有一天我遇到您的代码,则不必检查函数/方法体。
scriptin

3

我认为这不是一个坏主意!与这种最普遍的看法相反,正如罗伯特·哈维(Robert Harvey)所指出的那样,像Java这样的静态类型语言正是针对您所要求的情况引入了泛型。实际上,Java会尝试在编译时保持(尽可能)类型安全,有时泛型会避免代码重复,为什么?因为您可以编写处理/返回不同类型的相同方法或相同类。我将仅举一个非常简短的示例来说明这个想法:

Java 1.4

public static Boolean getBoolean(String property){
    return (Boolean) properties.getProperty(property);
}
public static Integer getInt(String property){
    return (Integer) properties.getProperty(property);
}

Java 1.5+

public static <T> getValue(String property, Class<T> clazz) throws WhateverCheckedException{
    return clazz.getConstructor(String.class).newInstance(properties.getProperty(property));
}
//the call will be
Boolean b = getValue("useProxy",Boolean.class);
Integer b = getValue("proxyPort",Integer.class);

在动态类型化语言中,由于在编译时没有类型安全性,因此您可以完全自由地编写适用于许多类型的相同代码。由于即使使用静态类型的语言也引入了泛型来解决此问题,因此很明显地暗示着编写一种以动态语言返回不同类型的函数并不是一个坏主意。


另一个不加解释的下注!感谢SE社区!
thermz 2014年

2
有同情+1。您应该尝试以更清晰,直接的答案来打开答案,并避免对不同的答案发表评论。(我同意,但我会再给您+1,但我只能给一个。)
DougM 2014年

3
泛型在动态类型的语言中不存在(据我所知)。他们没有理由这么做。大多数泛型都是特例,其中“容器”比其中的有趣(这就是为什么某些语言(如Java)实际上使用类型擦除)的原因。泛型类型实际上是它自己的类型。没有实际泛型类型的泛型方法(如此处的示例)几乎总是只是分配和广播语义的语法糖。这个问题真正问的不是一个特别引人注目的例子。
Aaronaught

1
我尝试使用一些句法:“即使引入了静态类型的语言来解决此问题,也很明显地暗示了编写一种以动态语言返回不同类型的函数并不是一个坏主意。” 现在好多了?查看Robert Harvey的答案或BRPocock的评论,您将意识到泛型的示例与此问题有关
thermz 2014年

1
别难过,@ thermz。不赞成投票的人通常对读者的看法要多于对作家的看法。
Karl Bielefeldt 2014年

2

开发软件基本上是管理复杂性的艺术和技巧。您尝试在可以承受的范围内缩小系统范围,并在其他方面限制选项。Function的接口是协定,它通过限制处理任何代码所需的知识来帮助管理代码的复杂性。通过返回不同的类型,您可以通过将返回的所有不同类型的接口添加到函数接口并添加关于返回哪个接口的明显规则来显着扩展该函数的接口。


1

Perl经常使用它,因为函数的作用取决于它的上下文。例如,一个函数可以在列表上下文中使用时返回一个数组,或者在期望标量值的地方使用时返回该数组的长度。在本教程中,这是“ perl上下文”的首创,如果您这样做:

my @now = localtime();

然后@now是一个数组变量(@就是这个意思),它将包含一个数组,如(40,51,20,9,0,109,5,8,0)。

如果改为以某种方式调用该函数,则其结果必须是一个标量,并且($变量为标量):

my $now = localtime();

那么它会执行完全不同的操作:$ now将类似于“ Fri Jan 9 20:51:40 2009”。

我可以想到的另一个示例是实现REST API,其中返回的格式取决于客户端所需的内容。例如,HTML,JSON或XML。尽管从技术上讲,这些都是字节流,但是想法很相似。


您可能要提到在perl中执行此操作的特定方式-wantarray ...也许链接到一些讨论(无论其好坏)- 使用wantarray在

碰巧的是,我正在使用自己构建的Java Rest API来处理这个问题。我使一种资源可以返回XML或JSON。在这种情况下,最低的公分母类型是String。现在人们正在要求有关返回类型的文档。如果我返回一个类,java工具可以自动生成它,但是由于我选择了String不能自动生成文档的信息。事后看来,我希望每种方法都返回一种类型。
Daniel Kaplan 2014年

严格来说,这相当于过载。换句话说,存在多个联合功能,并且根据调用的具体情况,选择一个或另一个版本。
2014年

1

在动态土地上,一切都是关于鸭式打字的。最负责任的公开/面向公众的事情是将可能不同的类型包装在为它们提供相同接口的包装器中。

function ThingyWrapper(thingy){ //a function constructor (class-like thingy)

    //thingy is effectively private and persistent for ThingyWrapper instances

    if(typeof thingy === 'array'){
        this.alertItems = function(){
            thingy.forEach(function(el){ alert(el); });
        }
    }
    else {
        this.alertItems = function(){
            for(var x in thingy){ alert(thingy[x]); }
        }
    }
}

function gimmeThingy(){
    var
        coinToss = Math.round( Math.random() ),//gives me 0 or 1
        arrayThingy = [1,2,3],
        objectThingy = { item1:1, item2:2, item3:3 }
    ;

    //0 dynamically evaluates to false in JS
    return new ThingyWrapper( coinToss ? arrayThingy : objectThingy );
}

gimmeThingy().alertItems(); //should be same every time except order of numbers - maybe

有时不使用通用包装就抛出不同的类型可能是有意义的,但老实说,在编写JS的7年间,我发现经常这样做并不合理或不方便。通常,这是我在封闭环境(例如将对象放在一起的对象内部)中执行的操作。但这并不是我经常做的事情,以至于没有任何例子可以想到。

通常,我建议您不要再考虑类型了。您需要使用动态语言处理类型。不再。这就是重点。不要检查每个参数的类型。只有在相同的方法可能以不明显的方式产生不一致的结果的环境中,我才会尝试这样做(因此绝对不要那样做)。但这不是重要的类型,而是您给我的工作方式。

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.