所有编程语言都有其设计缺陷,这仅仅是因为没有一种语言像大多数(所有其他)事物一样是完美的。除此之外,在您作为程序员的历史上,哪种编程语言的设计错误最让您感到烦恼?
请注意,如果一种语言是“不好的”,仅仅是因为它不是针对特定事物而设计的,那不是设计缺陷,而是设计的功能,因此,请不要列出这种烦人的语言。如果一种语言不适合其设计目的,那当然是设计中的缺陷。实施特定的事物和内幕事物也不重要。
所有编程语言都有其设计缺陷,这仅仅是因为没有一种语言像大多数(所有其他)事物一样是完美的。除此之外,在您作为程序员的历史上,哪种编程语言的设计错误最让您感到烦恼?
请注意,如果一种语言是“不好的”,仅仅是因为它不是针对特定事物而设计的,那不是设计缺陷,而是设计的功能,因此,请不要列出这种烦人的语言。如果一种语言不适合其设计目的,那当然是设计中的缺陷。实施特定的事物和内幕事物也不重要。
Answers:
我最大的烦恼之一是switch
如果您忘记使用C语言派生的案例,默认情况下会陷入下一个案例break
。我知道这在非常低级的代码(例如Duff's Device)中很有用,但通常不适用于应用程序级代码,并且是编码错误的常见来源。
我记得大约在1995年,当我第一次阅读有关Java的细节时,当我谈到有关该switch
语句的部分时,我非常失望,因为他们保留了默认的默认行为。这只是用另一个名字而switch
荣耀goto
。
switch
不必那样做。例如,Ada的case / when语句(等效于switch / case)不具有穿透行为。
switch
与C无关的语言中的类似语句不必那样工作。但是如果你使用C风格的控制流({
... }
,for (i = 0; i < N; ++i)
,return
等),语言推理会使人想到switch
要像C的工作,并给予它的Ada /帕斯卡尔/ BASIC样的语义会迷惑人。C#由于相同的原因需要break
in switch
语句,尽管通过禁止静默失败来减少出错的可能性。(但我希望您能写些文章fall;
而不是丑陋的文章goto case
。)
switch
允许失败。大多数情况下,fallthrough都不是故意的。
我从未真正喜欢使用C派生的语言=
进行赋值和==
相等性测试。混乱和错误的可能性过高。而且甚至不要让我开始使用===
Javascript。
更好的是:=
分配和=
平等测试。语义本来可以与今天的语义完全相同,其中赋值是一个也会产生值的表达式。
x<-5
)相比小于。C程序员不会容忍这种必需的空格:)
:=
,==
因为忘记了这太容易了,:
并且不会像今天已经忘记的情况那样被通知(尽管已恢复),所以不会被通知=
。我感谢编译器对此的警告...
=
而==
不是在读书,因为他们是不同的符号。这是书面形式,并确保您选择正确的人。
的选择+
在Javascript两个除了和字符串连接是一个可怕的错误。由于值是无类型的,因此这导致拜占庭规则,这些规则+
将根据每个操作数的确切内容确定是添加还是串联。
在一开始就引入一个全新的运算符(例如$
用于字符串连接)很容易。
+
作为强类型语言中的字符串连接运算符,这很有意义。问题是Javascript使用它,但是没有强类型输入。
+
级联没有意义,因为级联绝对不是可交换的。就我而言,这是一种虐待。
*
运算符是一种滥用?
我发现JavaScript默认是全局性的主要问题,如果您不使用JSLint或类似的语言,通常是bug的来源
var
声明变量就可以使用。而且不要告诉我它是太多类型的,因为Java迫使您两次声明所有类型,而没有人抱怨它是一个糟糕的设计选择。
std::map<KEY, VALUE>::const_iterator
在C ++中声明足够多的变量,然后再将auto
其添加到该语言中。
"use strict"
es5中添加的指令将未声明的引用更改为错误。
C和C ++中的预处理器是一个巨大的难题,它会像筛子一样泄漏出抽象的内容,通过大鼠的#ifdef
语句嵌套来鼓励意大利面条代码,并需要极其难以理解的ALL_CAPS
名称来解决其局限性。这些问题的根源在于它在文本级别而不是句法或语义级别运行。对于它的各种用例,应该已经用真实的语言功能代替了它。这是一些示例,尽管可以肯定地用C ++,C99或非官方但事实上的标准扩展解决了其中一些问题:
#include
问题,但是后来发明了模块系统!C和C ++旨在最大程度地向后兼容:/
#include
黑客攻击相当时,才可以将其视为设计错误。
可以用数百种语言列出数百种错误,但是从语言设计的角度来看,IMO并不是一种有用的练习。
为什么?
因为在一种语言中可能会出错的东西在另一种语言中不会出错。例如:
有一些经验教训要学习,但是这些经验教训很少是明确的,要理解它们,您必须了解技术上的权衡...以及历史背景。(例如,繁琐的Java泛型实现是出于维持向后兼容性的最重要业务要求的结果。)
IMO,如果您认真设计一种新的语言,则需要实际使用多种现有语言(并研究历史语言)...并自己确定错误是什么。而且您需要记住,每种语言都是在特定的历史背景下设计的,以满足特定的需求。
如果有一般的课程要学习,那么它们处于“元”级别:
C和C ++:所有那些没有任何意义的整数类型。
特别是char
。是文本还是小整数?如果是文本,是“ ANSI”字符还是UTF-8代码单元?如果是整数,是带符号的还是无符号的?
int
原本是“本机”大小的整数,但在64位系统上则不是。
long
可以大于或等于int
。它可能是也可能不是指针的大小。无论是32位还是64位,这都是编译器编写者的任意决定。
绝对是1970年代的语言。在Unicode之前。在64位计算机之前。
null
。
它的发明者托尼·霍尔(Tony Hoare)称其为“十亿美元的错误”。
它是60年代在ALGOL中引入的,并存在于当今大多数常用的编程语言中。
更好的选择,像OCaml中和Haskell语言中使用,是也许。一般的想法是对象引用不能为空/空/不存在,除非有明确的指示可能是空/空/不存在。
(尽管托尼的谦逊很棒,但我认为几乎每个人都会犯同样的错误,而他恰好是第一个。)
maybe
视为null选择加入,而null例外是选择退出。当然,关于运行时错误和编译时错误之间的区别也有很多话要说,但是null本质上是注入行为这一事实本身就值得注意。
我感到设计PHP的人没有使用普通的键盘,甚至没有使用colemak键盘,因为他们应该已经意识到自己在做什么。
Who::in::their::right::mind::would::do::this()
?该::
运营商需要在按住Shift键,然后两个关键印刷机。真是浪费能源。
虽然->这个->不是->不是->更好。这还需要三个按键,并且在两个符号之间进行切换。
$last = $we.$have.$the.$dumb.'$'.$character
。美元符号使用了很多次,并且要求奖品的使用范围一直延伸到键盘的最顶端,同时按下Shift键。
他们为什么不能将PHP设计为使用键入速度更快的键?为什么不能we.do.this()
或不让var以仅需要一次按键的键开头-或根本不需要(JavaScript)并仅预定义所有var(就像我必须为E_STRICT所做的那样)!
我不是慢打字员-但这只是一个la脚的设计选择。
I::have::nothing::against::this->at.all()
在asp.net中使用桌面启发式表单。
总是感觉到了错误,并妨碍了网络的实际运行。值得庆幸的是,asp.net-mvc并非以同样的方式受苦,尽管他的灵感来自Ruby等。
对我来说,这是PHP在其标准库中绝对缺少命名和参数排序约定。
尽管JASS在释放/删除被引用对象之后必须取消引用(否则引用会泄漏并且会丢失几个字节的内存)的必要性更加严重,但是由于JASS是单一用途语言,因此并不是那么关键。
我面临的最大设计缺陷是python最初并不是像python 3.x那样设计的。
delete
和delete[]
运算符的原因。
Java中的原始类型。
他们打破了一切都是其后裔的原则,java.lang.Object
从理论的角度来看,这导致了语言规范的额外复杂性,并且从实践的角度来看,它们使集合的使用极其繁琐。
自动装箱有助于减轻实际缺陷,但代价是使规范变得更加复杂并引入了巨大的香蕉皮:现在,您可以从看起来像简单的算术运算的操作中获得空指针异常。
我最了解Perl,所以我选择它。
Perl尝试了许多想法。有些很好。有些不好。有些是原创的,没有充分的理由复制。
一种是上下文的概念-每个函数调用都在列表或标量上下文中进行,并且可以在每种上下文中执行完全不同的操作。正如我在http://use.perl.org/~btilly/journal/36756中指出的那样,这会使每个API变得复杂,并经常导致Perl代码中的细微设计问题。
接下来是将语法和数据类型完全绑定的想法。这导致了领带的发明,以允许对象伪装成其他数据类型。(使用重载也可以达到相同的效果,但在Perl中,tie是更常见的方法。)
许多语言犯的另一个常见错误是,首先提供动态范围而不是词汇。以后很难恢复此设计决策,并导致持久的疣。Perl中这些疣的经典描述是http://perl.plover.com/FAQs/Namespaces.html。请注意,这是在Perl添加our
变量和static
变量之前编写的。
人们理所当然地在静态类型与动态类型上存在分歧。我个人喜欢动态打字。但是,重要的是要有足够的结构来捕获错别字。Perl 5严格地做到了这一点。但是Perl 1-4弄错了。其他几种语言还具有执行严格检查的功能。只要您擅长执行棉绒检查,就可以接受。
如果您正在寻找更多坏主意(很多),请学习PHP并研究其历史。我最喜欢的过去错误(很久以前已修复,因为它导致了很多安全漏洞)默认是允许任何人通过传递表单参数来设置任何变量。但这远非唯一的错误。
JavaScript对代码块和对象文字的含糊不清。
{a:b}
可以是代码块,其中a
是标签,b
是表达式;或者它可以定义一个对象,其属性a
具有值b
a
。
我将回到FORTRAN和空白不敏感状态。
它遍及整个规范。该END
卡必须定义为在7-72列中依次具有'E','N'和'D'的卡,并且没有其他非空白,而不是适当地带有“ END”的卡专栏,仅此而已。
它导致了容易的语法混乱。 DO 100 I = 1, 10
是一个循环控制语句,而是DO 100 I = 1. 10
一个将值1.1分配给名为的变量的语句DO10I
。(事实上,变量可以在不声明的情况下创建,其类型取决于它们的首字母。)与其他语言不同,没有其他方法可以使用空格分隔标记以消除歧义。
它还允许其他人编写真正令人困惑的代码。有理由为什么永远不会重复使用FORTRAN的此功能。
(test ? a : b)
,e)坚持使用时**
,f)无法处理区分大小写的行为。这主要是由于50年代的按键打孔。
我相信DSL(特定于域的语言),我对一种语言的重视是,如果它允许我在其之上定义DSL。
在Lisp中有宏-大多数人和我一样都认为这是一件好事。
在C和C ++中,存在宏-人们抱怨它们,但是我能够使用它们定义DSL。
在Java中,它们被遗漏了(因此在C#中被遗漏了),并且声明缺少它们是一种优点。当然可以,您可以拥有智能感知能力,但是对我来说,这只是一件小事。要进行DSL,我必须手动扩展。这很痛苦,即使让我用更少的代码来做更多的事情,也使我看起来像一个糟糕的程序员。
cdr
列表的,并从列表中形成一个lambda闭包(即连续),并将其作为参数传递给car
列表的。当然,这是递归完成的,将对条件,循环和函数调用“做正确的事”。然后,“选择”功能就变成了正常循环。不漂亮,但功能强大。问题是,它使创建过度嵌套的循环变得非常容易。
陈述,每种具有陈述的语言。它们不执行您无法使用表达式执行的操作,并阻止您执行很多操作。?:
三元运算符的存在只是必须尝试绕开它们的一个示例。在JavaScript中,它们特别令人讨厌:
// With statements:
node.listen(function(arg) {
var result;
if (arg) {
result = 'yes';
} else {
result = 'no';
}
return result;
})
// Without:
node.listen(function(arg) if (arg) 'yes' else 'no')
unit
类型(aka ()
)而不是语句的语言也需要特别注意以确保它们不会引发警告或表现异常。
对我来说,正是设计问题困扰了所有源自C的语言。即“ 悬空 ”。这个语法问题应该已经用C ++解决了,但是已经在Java和C#中实现了。
我认为到目前为止的所有答案都指向许多主流语言的单一失败:
在不影响向后兼容性的情况下,无法更改核心语言。
如果解决了这个问题,那么其他所有难题都可以解决。
编辑。
可以通过使用不同的名称空间在库中解决此问题,并且您可以设想对语言的大多数核心执行类似的操作,尽管这可能意味着您需要支持多个编译器/解释器。
最终,我认为我不知道如何以完全令人满意的方式解决问题,但这并不意味着不存在解决方案,或者无法完成更多工作
由于希望在添加泛型时保持向后兼容性,因此Java和C#的类型系统都存在烦人的问题。Java不喜欢混合泛型和数组。C#不允许使用一些有用的签名,因为您不能将值类型用作界限。
作为后者的示例,请考虑
公共静态T Parse <T>(Type <T> type,string str)其中T:枚举并排或替换
公共静态对象Parse(类型,字符串str)在里面
Enum
课堂上会允许MyEnum e = Enum.Parse(typeof(MyEnum),str);而不是重言式的
MyEnum e =(MyEnum)Enum.Parse(typeof(MyEnum),str);
tl; dr:开始设计类型系统时要考虑参数多态性,而不要在发布版本1之后考虑。
MyMethod<T>(T value) where T : struct, IComparable, IFormattable, IConvertible
但是您仍然必须测试枚举,这是一个hack。我认为C#泛型的最大不足是不支持更高的种类,这确实会使该语言具有一些很酷的概念。
我觉得自己很容易发火,但是我真的很讨厌在C ++中通过引用传递普通的旧数据类型的能力。我只是有点讨厌能够通过引用传递复杂类型。如果我正在查看一个函数:
void foo()
{
int a = 8;
bar(a);
}
从调用点来看,没有办法告诉bar
,可以在完全不同的文件中定义的是:
void bar(int& a)
{
a++;
}
有人可能会争辩说,这样做可能只是一个糟糕的软件设计,不能怪罪该语言,但我不喜欢该语言首先允许您这样做。使用指针并调用
bar(&a);
更具可读性。
当我学习COBOL时,ALTER语句仍然是标准的一部分。简而言之,该语句使您可以在运行时修改过程调用。
危险是您可能将此语句放在很少访问的晦涩难懂的代码部分中,并且有可能完全改变程序其余部分的流程。使用多个ALTER语句,您几乎不可能随时了解您的程序在做什么。
我的大学讲师非常强调说,如果他在我们的任何程序中看到该陈述,他都会自动使我们失望。
v() { if (not alreadyCalculatedResult) { result = long(operation); alreadyCalculatedResult = true; } result; }
说v() { result = long(operation); v = () => result; result; }
编程语言最严重的缺点还没有得到很好的定义。我记得一个例子是C ++,它的起源是:
我记得,花了大约十年的时间才使C ++定义得足够好,以使其与C一样具有专业上的可靠性。这种事情永远都不会发生。
我认为有其他事情(应该以不同的答案回答)是一种以上的“最佳”方式来完成某些常见任务。(还是)C ++,Perl和Ruby就是这种情况。
C ++中的类是该语言中的某种强制设计模式。
实际上,结构和类之间在运行时没有区别,要理解“信息隐藏”的真正编程优势到底是什么,真是令人困惑,以至于我想把它放在那里。
我为此会感到沮丧,但是无论如何,C ++编译器很难编写这种语言,感觉就像是一个怪物。
尽管每种语言都有其缺点,但一旦您了解它们,就不会造成麻烦。除了这对:
复杂的语法加上冗长的API
对于像Objective-C这样的语言,尤其如此。该语法不仅非常复杂,而且API使用的函数名称如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
我全力以赴,做到明确明确,但这很荒谬。每次我使用xcode坐下时,都会感到自己像个n00b,这真令人沮丧。
tableView:cellForRowAtIndexPath:
,在我看来这是非常可描述的。
(u)int_least8_t
)的概念。对于小整数,符号完全有意义,但对于字符则完全没有意义。
char*
像C-String一样强制转换为...,他们会感到非常困惑。
sbyte
,byte
和char
类型。