前言

这篇文章是在《游戏引擎架构》这本书的推荐里看到的,看完以后觉得作者的观点和我平时的想法非常接近,所以决定翻译一下。我看到了网上有这篇文章的纯中文版本,但是是机翻的。在这里你将看到的是我花费了大把时间,一句话一句话翻译过来的版本。为了方便读者理解,我将原文也拷贝了过来,如果我哪里翻译的不准确,或者让你迷糊的,可以直接看原文。

这篇文章的要讲的内容并不复杂,但是写的确实很啰嗦,或许是为了让读者更明白他想要表达的观点吧。如果你不支持作者的观点,也可以做为一个故事来看一看,毕竟有智慧的人是能够允许其他声音存在的。

原文链接:https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/

正文

Way back in September 1983, I started my first real job, working at Oranim, a big bread factory in Israel that made something like 100,000 loaves of bread every night in six giant ovens the size of aircraft carriers.

The first time I walked into the bakery I couldn’t believe what a mess it was. The sides of the ovens were yellowing, machines were rusting, there was grease everywhere.

“Is it always this messy?” I asked.

“What? What are you talking about?” the manager said. “We just finished cleaning. This is the cleanest it’s been in weeks.”

Oh boy.

It took me a couple of months of cleaning the bakery every morning before I realized what they meant. In the bakery, clean meant no dough on the machines. Clean meant no fermenting dough in the trash. Clean meant no dough on the floors.

Clean did not mean the paint on the ovens was nice and white. Painting the ovens was something you did every decade, not every day. Clean did not mean no grease. In fact there were a lot of machines that needed to be greased or oiled regularly and a thin layer of clean oil was usually a sign of a machine that had just been cleaned.

早在1983年9月,就开始了我的第一份正式的工作,在Oranim这家以色列的大型面包工厂,每天晚上我要在六个像航空母舰一样大的烤箱里考出100,000个面包。

当我第一次走进烘焙坊的时候,它的脏乱让我惊呆了。烤箱的侧面已经发黄,机器生着锈,到处都是油。

“它一直都这么脏吗?”我问道。

“啥?你在说啥?”经理说:“我们刚刚打扫完卫生,这是几周以来最干净的时候了。”

卧槽。

在我意识到他们的用意之前,我花费了几个月的时间来打扫烘焙坊,而且是每天早晨。在烘焙坊,干净意味着机器上没有面团。干净意味着不能在垃圾中发酵面团。干净意味着地板上没有面团。

干净并不意味着烤箱上的油漆又好又白。给烤箱刷漆是应该十年做一次的,而不是每天。干净也不意味着没有油。事实上有很多机器需要定期上润滑油或者防护油,一层薄薄的防护油通常是机器刚刚清洁过的标志。

The whole concept of clean in the bakery was something you had to learn. To an outsider, it was impossible to walk in and judge whether the place was clean or not. An outsider would never think of looking at the inside surfaces of the dough rounder (a machine that rolls square blocks of dough into balls, shown in the picture at right) to see if they had been scraped clean. An outsider would obsess over the fact that the old oven had discolored panels, because those panels were huge. But a baker couldn’t care less whether the paint on the outside of their oven was starting to turn a little yellow. The bread still tasted just as good.

After two months in the bakery, you learned how to “see” clean.

Code is the same way.

When you start out as a beginning programmer or you try to read code in a new language it all looks equally inscrutable. Until you understand the programming language itself you can’t even see obvious syntactic errors.

During the first phase of learning, you start to recognize the things that we usually refer to as “coding style.” So you start to notice code that doesn’t conform to indentation standards and Oddly-Capitalized variables.

在烘焙坊清洁的完整理念是必须要学习的。外行人是不可能走进来判断这里是否被清洁过的。外行人是不可能想到看一看团面机(这是一种将方形的面团滚揉成球状的机器,如右图所示(译者注:上图))的内表面是否被刮干净的。外行人会为旧烤箱的面板褪色而困扰,因为那些面板很大。但是烘焙师不会关心烤箱外面的面板是否已经开始发黄。面包吃起来还是棒棒的。

在烘焙坊两个月以后,你懂得了干净的含义。

代码也是一样的。

当你刚刚成为一个程序员或者是尝试阅读一种新的语言写的代码时,都是同样难以理解的。在你学会这门编程语言之前你甚至看不出那些明显的语法错误。

在你学习的第一个阶段里,你开始意识到什么是我们通常所说的“代码风格”。因此你开始注意代码中不遵守规则的缩进和奇怪的使用大写的变量。

It’s at this point you typically say, “Blistering Barnacles, we’ve got to get some consistent coding conventions around here!” and you spend the next day writing up coding conventions for your team and the next six days arguing about the One True Brace Style and the next three weeks rewriting old code to conform to the One True Brace Style until a manager catches you and screams at you for wasting time on something that can never make money, and you decide that it’s not really a bad thing to only reformat code when you revisit it, so you have about half of a True Brace Style and pretty soon you forget all about that and then you can start obsessing about something else irrelevant to making money like replacing one kind of string class with another kind of string class.

As you get more proficient at writing code in a particular environment, you start to learn to see other things. Things that may be perfectly legal and perfectly OK according to the coding convention, but which make you worry.

For example, in C:

char* dest, src;

This is legal code; it may conform to your coding convention, and it may even be what was intended, but when you’ve had enough experience writing C code, you’ll notice that this declares dest as a char pointer while declaring src as merely a char, and even if this might be what you wanted, it probably isn’t. That code smells a little bit dirty.

就这点而言,你通常会说:“这都是啥,我们必须要统一代码风格!”然后你将花费接下来的一天时间为你的团队编写代码规范,在接下来的六天里为了‘正确的大括号风格’而争吵,在接下来的三周里重写旧代码以使得他们符合‘正确的大括号风格’,直到你的经理抓住你并且对你吼叫,说你将时间浪费在了永远都不会赚钱的事情上,当你重新去看那些代码时你认为只对代码进行格式调整并不真的是件坏事。你有了大约一半的符合‘正确的大括号风格’的代码,然后很快你就会忘记这些事情,并且开始纠结于另一些与赚钱无关的事情。比如用一种String类替换另一种String类。

当你对写代码更熟练以后,你开始学学着去关注其他的事情。这些事情也许非常符合语法要求,也非常符合编码规范,但是他们却让你担心。

举个栗子,在C语言中:

char* dest, src;

这段代码语法正确,它也应该是符合你的代码规范,并且甚至可能是故意为之的,但是当你对C语言编码有足够多的经验时,你就会注意到声明的dest变量是一个char指针,而src则是一个char,这可能就是你想要的,也可能不是你想要的。这样的代码就很糟糕。

Even more subtle:

if (i != 0)
    foo(i);

In this case the code is 100% correct; it conforms to most coding conventions and there’s nothing wrong with it, but the fact that the single-statement body of the ifstatement is not enclosed in braces may be bugging you, because you might be thinking in the back of your head, gosh, somebody might insert another line of code there

if (i != 0)
    bar(i);
    foo(i);

… and forget to add the braces, and thus accidentally make foo(i)unconditional! So when you see blocks of code that aren’t in braces, you might sense just a tiny, wee, soupçon of uncleanliness which makes you uneasy.

再来个更精妙的栗子:

if (i != 0)
    foo(i);

在这种情况下这段代码是100%正确的,它也符合大多数的编码规范,并且也不会有什么错误,但事实上if语句声明的主体部分没有被大括号括起来会导致产生bug,因为你大概是是在用屁股思考问题吧,老天爷啊,其他人可能要在里面插入其他的代码

if (i != 0)
    bar(i);
    foo(i);

并且忘记补充大括号,从而导致foo(i)语句缺少了判断条件!当你在看代码的时候,缺少大括号,你或许会因为到一丢丢的不整洁而感到不安。

OK, so far I’ve mentioned three levels of achievement as a programmer:

1. You don’t know clean from unclean.

2. You have a superficial idea of cleanliness, mostly at the level of conformance to coding conventions.

3. You start to smell subtle hints of uncleanliness beneath the surface and they bug you enough to reach out and fix the code.

There’s an even higher level, though, which is what I really want to talk about:

4. You deliberately architect your code in such a way that your nose for uncleanliness makes your code more likely to be correct.

This is the real art: making robust code by literally inventing conventions that make errors stand out on the screen.

好了,到目前为止,我已经提及到了关于程序员的三个层次:

1.你不知道什么是整洁和不整洁。

2.你有了一些关于代码整洁的肤浅的想法,主要是在代码规范层面的。

3.你开始注意到潜藏的不整洁的暗示,并且足以让你感到忧虑并着手修复它们。

当然还有更高的层次,这也是我真正想要说的:

4.你应该以这样的方式架构你的代码:通过对不整洁的嗅觉使你的代码看起来更加正确。(译者注,实在不知道该怎么翻译了,也不必非得纠结这句话,后面的解释更直观...)

这才是真正的艺术:设计一套能够让错误显而易见的代码规范,以使你的代码更健壮。

So now I’ll walk you through a little example, and then I’ll show you a general rule you can use for inventing these code-robustness conventions, and in the end it will lead to a defense of a certain type of Hungarian Notation, probably not the type that makes people carsick, though, and a criticism of exceptions in certain circumstances, though probably not the kind of circumstances you find yourself in most of the time.

But if you’re so convinced that Hungarian Notation is a Bad Thing and that exceptions are the best invention since the chocolate milkshake and you don’t even want to hear any other opinions, well, head on over to Rory’s and read the excellent comix instead; you probably won’t be missing much here anyway; in fact in a minute I’m going to have actual code samples which are likely to put you to sleep even before they get a chance to make you angry. Yep. I think the plan will be to lull you almost completely to sleep and then to sneak the Hungarian=good, Exceptions=bad thing on you when you’re sleepy and not really putting up much of a fight.

现在我将给你展示一个小例子,然后教给你一条通用规则,你可以用它制定一套保证代码健壮的代码规范,在最后将会引导至为一种名为匈牙利命名法的辩护中,这大概不会让你头晕吧,以及在某些情况下对异常处理的批评,虽然大多数时候这可能会让你迷失自己。

但是,如果你确信匈牙利命名法是坏事,异常处理是在巧克力奶昔以后最好的发明,并且你不想听到其他的任何声音,那么跟着Rory去读一读这篇文章吧,你也许不会错过什么吧。事实上,在一分钟以内,我就会给你展示一段实际的代码。也许在他们把你惹毛之前,就能够让你昏昏欲睡。我想这个计划差不多可以让你平静的入睡并且悄悄的让你接受“匈牙利命名法就是好的,异常就是坏的”,而不是要跟我干一架。

An Example

Right. On with the example. Let’s pretend that you’re building some kind of a web-based application, since those seem to be all the rage with the kids these days.

Now, there’s a security vulnerability called the Cross Site Scripting Vulnerability, a.k.a. XSS. I won’t go into the details here: all you have to know is that when you build a web application you have to be careful never to repeat back any strings that the user types into forms.

So for example if you have a web page that says “What is your name?” with an edit box and then submitting that page takes you to another page that says, Hello, Elmer! (assuming the user’s name is Elmer), well, that’s a security vulnerability, because the user could type in all kinds of weird HTML and JavaScript instead of “Elmer” and their weird JavaScript could do nasty things, and now those nasty things appear to come from you, so for example they can read cookies that you put there and forward them on to Dr. Evil’s evil site.

举个栗子。假装你现在正在开发一款基于web的应用,因为它们看起来在孩子中非常流行。

现在,有一个漏洞名为跨站点脚本漏洞,简称XSS。我在这里不会给你详细介绍,你只需要知道当你在开发web应用的时候,一定要小心,绝不能将用户输入的表单中的任何字符串直接传回去。

举例来说,如果你的网页有这样的显示“你叫啥?”而且后面跟着一个输入框,提交以后会跳到另一个页面,并且显示“你好,Elmer!“(假如用户输入的是Elmer),这就是一个安全漏洞,因为用户可以输入一些奇怪的HTML或者JS而不是“Elmer”,然后那些奇怪的js脚本就可以做一些龌龊的事情,这些龌龊的事情看起来就像是你做的,比如说它们可以读取你的cookies并且发送给用心险恶的网站。

Let’s put it in pseudocode. Imagine that

s = Request("name")

reads input (a POST argument) from the HTML form. If you ever write this code:

Write "Hello, " & Request("name")

your site is already vulnerable to XSS attacks. That’s all it takes.

Instead you have to encode it before you copy it back into the HTML. Encoding it means replacing " with ", replacing > with >, and so forth. So

Write "Hello, " & Encode(Request("name"))

is perfectly safe.

All strings that originate from the user are unsafe. Any unsafe string must not be output without encoding it.

Let’s try to come up with a coding convention that will ensure that if you ever make this mistake, the code will just look wrong. If wrong code, at least, looks wrong, then it has a fighting chance of getting caught by someone working on that code or reviewing that code.

让我们用伪代码实现它。

s = Request("name")

想象一下这句话可以从HTML的表单输入(Post请求)中读取信息。如果你是这样实现的你的代码:

Write "Hello, " & Request("name")

你的网站就已经准备好被XSS攻击了。就是这句代码造成的。

你需要在复制并返回给HTML之前对数据进行encode操作。对数据进行encode操作意味着将  符号替换成    "     ,将 符号 >  替换成    >   如此等等。

Write "Hello, " & Encode(Request("name"))

这样写就很安全了。

来源于用户提交的字符串都是不安全的。任何不安全的字符串在没有encode之前都不能发送出去。

让我们尝试着提出这样一条代码规范,它可以保证在你犯了这样的错误时,能够一眼就看出错误来。如果代码是错误的,哪怕是看起来错误的,这样其他人在读到这段代码的时候就更有机会去修复存在的问题。

Possible Solution #1

One solution is to encode all strings right away, the minute they come in from the user:

s = Encode(Request("name"))

So our convention says this: if you ever see Request that is not surrounded by Encode, the code must be wrong.

You start to train your eyes to look for naked Requests, because they violate the convention.

That works, in the sense that if you follow this convention you’ll never have a XSS bug, but that’s not necessarily the best architecture. For example maybe you want to store these user strings in a database somewhere, and it doesn’t make sense to have them stored HTML-encoded in the database, because they might have to go somewhere that is not an HTML page, like to a credit card processing application that will get confused if they are HTML-encoded. Most web applications are developed under the principle that all strings internally are not encoded until the very last moment before they are sent to an HTML page, and that’s probably the right architecture.

We really need to be able to keep things around in unsafe format for a while.

OK. I’ll try again.

一号可能的方案

解决方案就是在刚刚从用户那里传过来的时候就对string内容进行encode操作:

s = Encode(Request("name"))

所以我们制定了这样的规则:如果你看到了没有在Encode内执行的Request,这段代码就是错误的。

你开始训练你的眼睛去寻找裸露的Request调用,因为这违反了上面的规则。

通过上面的方式你只要遵守这条约定,你就永远不会遇到XSS的bug。但是这并不一定是最好的架构。比如,你也许希望将用户的输入的字符串存储到数据库中,而且在数据库中存储那些被HTML执行了encode的数据是没有意义的,因为它们可能要用在不是HTML页面的地方,比如说处理信用卡的应用使用被编码过的信息就会呵呵了。大部分的web应用都遵循在将数据最终发送给HTML页面之前都不会对其进行编码的原则,这或许才是正确的架构吧。

看来我们真的需要让它们在不安全的状态下呆上一段时间了。

让我们再试一次吧。

Possible Solution #2

What if we made a coding convention that said that when you write out any string you have to encode it?

s = Request("name")

// much later:
Write Encode(s)

Now whenever you see a naked Write without the Encode you know something is amiss.

Well, that doesn’t quite work… sometimes you have little bits of HTML around in your code and you can’t encode them:

If mode = "linebreak" Then prefix = "<br>"

// much later:
Write prefix

This looks wrong according to our convention, which requires us to encode strings on the way out:

Write Encode(prefix)

二号可能的方案
是不是我们要制定这样的规则:当你在写入字符串时,必须对其进行编码?

s = Request("name")

// much later:
Write Encode(s)

现在当你看到没有在Encode方法中执行的Write方法时,你就知道,这里好像有什么猫饼。

它好像不灵...有时候我们的代码中需要在一些HTML代码段,这时就不能对他们进行encode:

If mode = "linebreak" Then prefix = "<br>"

// much later:
Write prefix

从约定的角度看这是错误的,根据要求我们必须这样写:

Write Encode(prefix)

But now the "<br>", which is supposed to start a new line, gets encoded to &lt;br&gt; and appears to the user as a literal < b r >. That’s not right either.

So, sometimes you can’t encode a string when you read it in, and sometimes you can’t encode it when you write it out, so neither of these proposals works. And without a convention, we’re still running the risk that you do this:

译者注(代码段10)

s = Request("name")

...pages later...
name = s

...pages later...
recordset("name") = name // store name in db in a column "name"

...days later...
theName = recordset("name")

...pages or even months later...
Write theName

Did we remember to encode the string? There’s no single place where you can look to see the bug. There’s no place to sniff. If you have a lot of code like this, it takes a ton of detective work to trace the origin of every string that is ever written out to make sure it has been encoded.

但是现在   "<br>"   这个换行符就被编码成了   &lt;br&gt;   并且会以这样的形式呈现给用户    < b r >   。这也是不对的。

所以,很多时候你不能在读取字符串的时候进行编码,很多时候你也不能在输出的时候进行编码,所以这些方案都是行不通的。但是如果没有规则,我们仍然会面临风险,如果你还是这样写的:

(代码段10)

你还记得要对string进行编码吗?这里已经没有能够让你识别bug的标记了,没有哪里让你去闻了。如果你有大量的代码是类似这样的,那么要追踪每一个要输出的字符串的来源以确保被encode过就需要做大量的检查工作。

The Real Solution

So let me suggest a coding convention that works. We’ll have just one rule:

All strings that come from the user must be stored in variables (or database columns) with a name starting with the prefix “us” (for Unsafe String). All strings that have been HTML encoded or which came from a known-safe location must be stored in variables with a name starting with the prefix “s” (for Safe string).

Let me rewrite that same code, changing nothing but the variable names to match our new convention.

译者注(代码段11)

us = Request("name")

...pages later...
usName = us

...pages later...
recordset("usName") = usName

...days later...
sName = Encode(recordset("usName"))

...pages or even months later...
Write sName

正式的解决方案

我来给出一个可行代码规范。我们只需要这样一条规则:

所有来自用户提交的字符串我们必须存储在以“us”(代表Unsafe String)为前缀的变量(或者是数据库列)中。所有已经被HTML执行过encode操作的或者是明确其来源安全的字符串必须存储在以“s”(代表Safe String)为前缀的变量中。

让我重写这段相同的代码,只改变其中的变量名以符合我们的新的规范。

(代码段11)

The thing I want you to notice about the new convention is that now, if you make a mistake with an unsafe string, you can always see it on some single line of code, as long as the coding convention is adhered to:

s = Request("name")

is a priori wrong, because you see the result of Request being assigned to a variable whose name begins with s, which is against the rules. The result of Request is always unsafe so it must always be assigned to a variable whose name begins with “us”.

us = Request("name")

is always OK.

usName = us

is always OK.

sName = us

is certainly wrong.

sName = Encode(us)

is certainly correct.

Write usName

is certainly wrong.ml/

Write sName

is OK, as is

Write Encode(usName)

Every line of code can be inspected by itself, and if every line of code is correct, the entire body of code is correct.

关于这条新的约定,我想让你注意的是,如果你因为使用了不安全的字符串而制造了一个错误,你就可以轻易的在代码中发现它,只要你遵守了这条代码规范:

s = Request("name")

这就是一个很明显的错误,因为Request的结果赋值给了违背我们原则,使用了以“s”前缀命名的变量。Request方法的结果是不安全的,所以它必须赋值给一个以“us”为前缀的变量。

us = Request("name")

这样写是可以的。(译者注:我个人不支持这种写法,除非这个属性的生命周期很短)

usName = us

这样写也是可以的。

sName = us

这样不行。

sName = Encode(us)

这样可以。

Write usName

这是不对的。

Write sName

这是对的,下面这种写法也行

Write Encode(usName)

每一行都可以只通过自己这一行来进行检查,如果每一行代码都是正确的,那么所有的代码就是正确的。(译者注:下划线这部分,本人不敢苟同,表示呵呵。)

Eventually, with this coding convention, your eyes learn to see the Write usXXX and know that it’s wrong, and you instantly know how to fix it, too. I know, it’s a little bit hard to see the wrong code at first, but do this for three weeks, and your eyes will adapt, just like the bakery workers who learned to look at a giant bread factory and instantly say, “jay-zuss, nobody cleaned inside(译者注:原文是insahd) rounduh fo-ah(译者注:这又是啥?放飞自我了吗?)! What the hayl kine a opparashun y’awls runnin’ heey-uh(译者注,这又是什么鬼哦)?”

In fact we can extend the rule a bit, and rename (or wrap) the Request and Encode functions to be UsRequest and SEncode… in other words, functions that return an unsafe string or a safe string will start with Us and S, just like variables. Now look at the code:

us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SEncode(recordset("usName"))
Write sName

See what I did? Now you can look to see that both sides of the equal sign start with the same prefix to see mistakes.

us = UsRequest("name") // ok, both sides start with US
s = UsRequest("name") // bug
usName = us // ok
sName = us // certainly wrong.
sName = SEncode(us) // certainly correct.

Heck, I can take it one step further, by naming Write to WriteS and renaming SEncode to SFromUs:

us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SFromUs(recordset("usName"))
WriteS sName

最后,基于这条代码规范,你的眼睛就能学会当看到Write usXXX 这样的代码时,就知道这是错误的,同时你也马上就能知道该怎样修复它。我知道,刚开始去发现这种错误还是有点困难的,但是坚持三周以后,你的眼睛就可以适应了,就像是大面包厂里的工人学会去检查卫生一样,能够马上就说出:“阿西吧,没有人清理一下内部吗!*&%#$&^%$#%”

事实上,我们可以对这条约定稍微扩展一下,将方法Request和Encode重命名(或者包装一下)为UsRequest和SEncode...换种说法就是,这些以"us"和"s"开头的方法返回了不安全的字符串或者是安全的字符串,就像是变量一样。现在再来看一下这些这段代码:

us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SEncode(recordset("usName"))
Write sName

瞅瞅我都做了啥?现在你已经可以通过观察等号两边的内容是否使用了一致的前缀来发现错误了。

us = UsRequest("name") // ok, both sides start with US
s = UsRequest("name") // bug
usName = us // ok
sName = us // certainly wrong.
sName = SEncode(us) // certainly correct.

牛逼吧,我还能更进一步,把Write重命名为WriteS,SEncode重命名为SFromUS:

us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SFromUs(recordset("usName"))
WriteS sName

This makes mistakes even more visible. Your eyes will learn to “see” smelly code, and this will help you find obscure security bugs just through the normal process of writing code and reading code.

Making wrong code look wrong is nice, but it’s not necessarily the best possible solution to every security problem. It doesn’t catch every possible bug or mistake, because you might not look at every line of code. But it’s sure a heck of a lot better than nothing, and I’d much rather have a coding convention where wrong code, at least, looked wrong. You instantly gain the incremental benefit that every time a programmer’s eyes pass over a line of code, that particular bug is checked for and prevented.

这使得错误更加明显了。你的眼睛将学会去分辨那些臭臭的代码,只需要通过正常的编码和阅读过程就能够帮助你发现那些潜在的安全性bug。

让错误代码容易辨识是很棒的,但是这并不一定适用于每一个安全性问题。他不会捕获到所有的bug和错误,因为你可能不会去查看每一行代码。但这肯定比什么都没有要好的多,而且我也希望能够有一个代码规范,至少让错误的代码看起来是错的。当码农们的眼睛扫描着一行行的代码时,一些特定的bug就会被及时的发现并修正,你将因此获得很多很多的好处。

A General Rule

This business of making wrong code look wrong depends on getting the right things close together in one place on the screen. When I’m looking at a string, in order to get the code right, I need to know, everywhere I see that string, whether it’s safe or unsafe. I don’t want that information to be in another file or on another page that I would have to scroll to. I have to be able to see it right there and that means a variable naming convention.

There are a lot of other examples where you can improve code by moving things next to each other. Most coding conventions include rules like:

  • Keep functions short.
  • Declare your variables as close as possible to the place where you will use them.
  • Don’t use macros to create your own personal programming language.
  • Don’t use goto.
  • Don’t put closing braces more than one screen away from the matching opening brace.

What all these rules have in common is that they are trying to get the relevant information about what a line of code really does physically as close together as possible. This improves the chances that your eyeballs will be able to figure out everything that’s going on.

In general, I have to admit that I’m a little bit scared of language features that hide things. When you see the code

i = j * 5;

… in C you know, at least, that j is being multiplied by five and the results stored in i.

通用规则

让错误更容易被发现,依赖于让正确的东西集中在屏幕的一个区域里。当我看到一个字符串时,为了确保它是正确的,我需要知道在我能够看到它的每一个地方,他是安全的或者不安全的。我不希望这些信息位于其他文件,或者翻页以后才能看到的其他页面里。我必须能够在这里看到它,这就是一条命名规范。

在通过把代码聚集在一起以改善代码质量方面有很多例子。大多数代码规范都遵守像这样的规定:

  • 方法的内容要尽量短
  • 变量的声明要尽可能靠近使用它的地方
  • 不要使用宏创建你自己的编程语言
  • 不要使用goto
  • 左大括号和对应的右大括号的间距不要超过一屏

这些规则的共同点在于,它们努力的把一行代码相关的所有内容聚集在一起。这使得你只需要眼球动移动就能够理解所有的东西了。

总之,我必须要承认我有点害怕那些可以隐藏东西的语言特性。当你看到这段代码时

i = j * 5;

在C语言中你至少能够知道,j和5相乘以后的结果存储在了i中。

But if you see that same snippet of code in C++, you don’t know anything. Nothing. The only way to know what’s really happening in C++ is to find out what types i and j are, something which might be declared somewhere altogether else. That’s because j might be of a type that has operator* overloaded and it does something terribly witty when you try to multiply it. And i might be of a type that has operator= overloaded, and the types might not be compatible so an automatic type coercion function might end up being called. And the only way to find out is not only to check the type of the variables, but to find the code that implements that type, and God help you if there’s inheritance somewhere, because now you have to traipse all the way up the class hierarchy all by yourself trying to find where that code really is, and if there’s polymorphism somewhere, you’re really in trouble because it’s not enough to know what type i and j are declared, you have to know what type they are right now, which might involve inspecting an arbitrary amount of code and you can never really be sure if you’ve looked everywhere thanks to the halting problem (phew!).

但是如果你在C++里看到这样的代码片段,你什么都了解不到。在C++里去了解真正发生了什么的唯一途径是找到ij的类型,这可能是在其他地方声明的。因为可能是运算符*的重载类型,当你试图对它进行乘法运算的时候它会做一些非常有趣的事。i可能事运算符=的重载类型,可能会因为类型不兼容而调用强制转换的方法。唯一有效的途径不仅要检查变量的类型,还要找到实现它的代码,如果上帝眷顾着你(译者注:说反话),让某些地方存在着继承,现在你就必须要靠自己去检查类的所有层次关系,以发现代码的真正实现位置,如果某些地方还存在多态性,那你的麻烦就大了,因为仅仅知道i和j的类型和声明还不够,你还必须要知道当前它们事什么东西,这样要检查的代码量就能是任意数了,并且,你永远都不能确定你已经看完了所有的地方(一万只草泥马!)。

When you see i=j*5 in C++ you are really on your own, bubby, and that, in my mind, reduces the ability to detect possible problems just by looking at code.

None of this was supposed to matter, of course. When you do clever-schoolboy things like override operator*, this is meant to be to help you provide a nice waterproof abstraction. Golly, j is a Unicode String type, and multiplying a Unicode String by an integer is obviously a good abstraction for converting Traditional Chinese to Standard Chinese, right?

The trouble is, of course, that waterproof abstractions aren’t. I’ve already talked about this extensively in The Law of Leaky Abstractions so I won’t repeat myself here.

Scott Meyers has made a whole career out of showing you all the ways they fail and bite you, in C++ at least. (By the way, the third edition of Scott’s book Effective C++ just came out; it’s completely rewritten; get your copy today!)

Okay.

I’m losing track. I better summarize The Story Until Now:

Look for coding conventions that make wrong code look wrong. Getting the right information collocated all together in the same place on screen in your code lets you see certain types of problems and fix them right away.

当你在C++中看到这样的代码时,你只能靠你自己了,而且我认为这样的东西降低了只看代码就发现其潜在问题的可能。

当然了,这些都不重要。当你像学生一样去写运算符重载的时候,只是为了提供一个好的安全的抽象。我的老天爷啊,"j"是个Unicode的字符串类型,将一个Unicode字符串类型的数据和一个整形数据相乘,明显的是一个将繁体中文转换成简体中文的很好的抽象,不是吗?(译者注:这是说反话)

问题在于,没有绝对安全的抽象。我之前就已经在抽象出错定律里讨论过了,在这里就不再重复了。

Scott Meyers把他整个C++职业生涯中失败的方式展示给了你。(顺便一提,第三版的《有效C++》刚刚出版;改动很大,今天刚买了一本)

好吧,跑题跑远了。我最好总结一下目前的内容:

找到让错误代码能够看出错误的代码规范。让相关的信息都集中一起,方便你迅速的找出问题并修复它。

I’m Hungary

So now we get back to the infamous Hungarian notation.

Hungarian notation was invented by Microsoft programmer Charles Simonyi. One of the major projects Simonyi worked on at Microsoft was Word; in fact he led the project to create the world’s first WYSIWYG word processor, something called Bravo at Xerox Parc.

In WYSIWYG word processing, you have scrollable windows, so every coordinate has to be interpreted as either relative to the window or relative to the page, and that makes a big difference, and keeping them straight is pretty important.

Which, I surmise, is one of the many good reasons Simonyi started using something that came to be called Hungarian notation. It looked like Hungarian, and Simonyi was from Hungary, thus the name. In Simonyi’s version of Hungarian notation, every variable was prefixed with a lower case tag that indicated the kind of thing that the variable contained.

I’m using the word kind on purpose, there, because Simonyi mistakenly used the word type in his paper, and generations of programmers misunderstood what he meant.

我是匈牙利人

现在让我们回到声名狼藉的匈牙利命名法。

匈牙利命名法是微软的程序员Charles Simonyi发明的。Simonyi在微软主要参与的项目是Word;事实上,他还带领团队创建了世界上第一个所见即所得的文本处理器,在Xerox Parc被称作Bravo。

在所见即所得的文本处理器中,窗口时可以滚动的,所以每一个坐标都必须分清楚是相对于窗口坐标的,还是相对于页面坐标的,这是区别很大的,把他们处理好是非常重要的。

我觉得把Simonyi所使用的这套东西叫做匈牙利命名法的一个最重要的原因是,这看起来就像是匈牙利语,而且Simonyoi就是来自匈牙利的。在Simonyi的匈牙利命名法的版本中,每一个变量的都会拥有小写的用来指示其内容性质(kind)的前缀。

我故意用Kind这个单词的原因(译者注:我把它翻译成性质的原因),是因为Simonyi误用了类型(Type)这个单词,使得一批批的码农误解了他的意思。

If you read Simonyi’s paper closely, what he was getting at was the same kind of naming convention as I used in my example above where we decided that us meant “unsafe string” and s meant “safe string.” They’re both of type string. The compiler won’t help you if you assign one to the other and Intellisense won’t tell you bupkis. But they are semantically different; they need to be interpreted differently and treated differently and some kind of conversion function will need to be called if you assign one to the other or you will have a runtime bug. If you’re lucky.

Simonyi’s original concept for Hungarian notation was called, inside Microsoft, Apps Hungarian, because it was used in the Applications Division, to wit, Word and Excel. In Excel’s source code you see a lot of rw and col and when you see those you know that they refer to rows and columns. Yep, they’re both integers, but it never makes sense to assign between them. In Word, I’m told, you see a lot of xl and xw, where xl means “horizontal coordinates relative to the layout” and xw means “horizontal coordinates relative to the window.” Both ints. Not interchangeable. In both apps you see a lot of cb meaning “count of bytes.” Yep, it’s an int again, but you know so much more about it just by looking at the variable name. It’s a count of bytes: a buffer size. And if you see xl = cb, well, blow the Bad Code Whistle, that is obviously wrong code, because even though xl and cb are both integers, it’s completely crazy to set a horizontal offset in pixels to a count of bytes.

如果你仔细的阅读了Simonyi写的东西,他想要的命名规范和我在之前的例子中使用的是一样的,像是我们用“us”指示不安全的字符串,“s”指示安全的字符串。它们的类型都(type)是string。如果把两者互换,编译器和智能提示都不会说什么。但是它们在语义上是不同的;对他们的理解和处理方式都是不一样的,如果要对他们进行互换就必须通过一些转换方法,否则就会有运行时bug出现。

Simonyi最初的匈牙利命名法在微软内部被称为应用型匈牙利语(Apps Hungarian),因为它被用在了应用程序部,也就是Word和Excel。在Excel的源码中,你可以看到大量的rw和col,当你看到它们时你就知道它们代表着row和column。就算它们都是int类型的,可是它们之间的转换也是完全没有意义的。在Word中,你看到大量的xl和xw,xl的意思是相对于布局的横坐标,xw的意思是型对于窗口的横坐标。同样是int类型的,但却是不能互换的。在这两个应用程序中,你能看到大量的cb,意思是字节长度。它也是int类型的,但是你只通过名字就能知道它的长度。它的长度代表的是缓冲区的大小。如果你看到了xl=cb,小口哨吹起来,这就是个明显的错误代码了,因为即使xl和cb都是int类型的,用某个缓冲区的长度给坐标偏移的像素值赋值,也是不可理喻的行为。

In Apps Hungarian prefixes are used for functions, as well as variables. So, to tell you the truth, I’ve never seen the Word source code, but I’ll bet you dollars to donuts there’s a function called YlFromYw which converts from vertical window coordinates to vertical layout coordinates. Apps Hungarian requires the notation TypeFromType instead of the more traditional TypeToType so that every function name could begin with the type of thing that it was returning, just like I did earlier in the example when I renamed Encode SFromUs. In fact in proper Apps Hungarian the Encode function would have to be named SFromUs. Apps Hungarian wouldn’t really give you a choice in how to name this function. That’s a good thing, because it’s one less thing you need to remember, and you don’t have to wonder what kind of encoding is being referred to by the word Encode: you have something much more precise.

Apps Hungarian was extremely valuable, especially in the days of C programming where the compiler didn’t provide a very useful type system.

But then something kind of wrong happened.

The dark side took over Hungarian Notation.

Nobody seems to know why or how, but it appears that the documentation writers on the Windows team inadvertently invented what came to be known as Systems Hungarian.

在应用型匈牙利语种前缀也用来修饰方法名。所以,虽然我从来没有见到过Word的源代码,但是我敢跟你打赌,它里面一定有一个方法名为YlFromYw用来将坐标从windows坐标系转换到Layout坐标系。应用型匈牙利语要求用TypeFromType格式的方法名替换传统的TypeToType,这样就可以通过开始的Type类型就知道方法的返回类型,就像是我在前面的例子中重命名Encode为SFromUs一样。在正规的应用型匈牙利语中Encode方法就必须重命名为SFromUs。应用型西班牙语在这个方法的命名上没有给你其他选择,这是件好事,因为你就少了一件事要做,同时你也不必再去在意Encode引用的是什么类型:你得到了更多更准确的信息。

应用型匈牙利语是非常有价值的,特别是在C语言盛行而编译器还不能提供有用的类别系统时。

但是后来出问题了。

黑暗势力占据了匈牙利命名法。

没有人知道原因,但是这确实发生了,Windows团队的写文档的人无意间发明了被大家了解的系统型匈牙利语(译者注:Sytems Hungarian语前面的AppsHungarian)。

Somebody, somewhere, read Simonyi’s paper, where he used the word “type,” and thought he meant type, like class, like in a type system, like the type checking that the compiler does. He did not. He explained very carefully exactly what he meant by the word “type,” but it didn’t help. The damage was done.

Apps Hungarian had very useful, meaningful prefixes like “ix” to mean an index into an array, “c” to mean a count, “d” to mean the difference between two numbers (for example “dx” meant “width”), and so forth.

Systems Hungarian had far less useful prefixes like “l” for long and “ul” for “unsigned long” and “dw” for double word, which is, actually, uh, an unsigned long. In Systems Hungarian, the only thing that the prefix told you was the actual data type of the variable.

This was a subtle but complete misunderstanding of Simonyi’s intention and practice, and it just goes to show you that if you write convoluted, dense academic prose nobody will understand it and your ideas will be misinterpreted and then the misinterpreted ideas will be ridiculed even when they weren’t your ideas. So in Systems Hungarian you got a lot of dwFoo meaning “double word foo,” and doggone it, the fact that a variable is a double word tells you darn near nothing useful at all. So it’s no wonder people rebelled against Systems Hungarian.

某些人在某些地方读到了Simonyi写的东西,他用的就是“type”这个词,所以就认为他的本意就是type,就像是class,就像是在类型系统里,就像是编译器做的类型检查一样。但是这不是他的本意。他非常小心的并且恰当的解释了“type”的意思,然并卵。伤害就这样发生了。

应用型匈牙利语是非常有用的,这些有意义的前缀,就像“ix”表示一个数组的下标,“c”表示count,“d”表示两个数的差异(例如dx代表width),如此种种。

系统型匈牙利语中前缀的作用就小多了,比如“l”表示long,“ul”表示无符号long,"dw"表示两个world,其实也就是无符号long。在系统型西班牙语中前缀能够传达的只有变量的数据类型。

虽然只是细微的差异,但这却完全误解了Simony的意图,就像是你写的复杂难懂的学术论文,没有人能够理解它,你的想法会被误解并,被错误理解出来的想法会遭到嘲笑,即便它已经不是你原来的想法。所以在系统型匈牙利语中存在着大量用dwFoo表示两个长度的啥啥啥,告诉你变量是两个长度对你来说,并没有什么卵用。难怪人们会反抗系统型匈牙利语。

Systems Hungarian was promulgated far and wide; it is the standard throughout the Windows programming documentation; it was spread extensively by books like Charles Petzold’s Programming Windows, the bible for learning Windows programming, and it rapidly became the dominant form of Hungarian, even inside Microsoft, where very few programmers outside the Word and Excel teams understood just what a mistake they had made.

And then came The Great Rebellion. Eventually, programmers who never understood Hungarian in the first place noticed that the misunderstood subset they were using was Pretty Dang Annoying and Well-Nigh Useless, and they revolted against it. Now, there are still some nice qualities in Systems Hungarian, which help you see bugs. At the very least, if you use Systems Hungarian, you’ll know the type of a variable at the spot where you’re using it. But it’s not nearly as valuable as Apps Hungarian.

The Great Rebellion hit its peak with the first release of .NET. Microsoft finally started telling people, “Hungarian Notation Is Not Recommended.” There was much rejoicing. I don’t even think they bothered saying why. They just went through the naming guidelines section of the document and wrote, “Do Not Use Hungarian Notation” in every entry. Hungarian Notation was so doggone unpopular by this point that nobody really complained, and everybody in the world outside of Excel and Word were relieved at no longer having to use an awkward naming convention that, they thought, was unnecessary in the days of strong type checking and Intellisense.

But there’s still a tremendous amount of value to Apps Hungarian, in that it increases collocation in code, which makes the code easier to read, write, debug, and maintain, and, most importantly, it makes wrong code look wrong.

系统型匈牙利语影响深远;并且作为标准贯穿于所有Windows开发文档中;而且通过像 Charles Petzold’s Programming Windows(作为学习Window编程的圣经,)这类书得到广泛的传播,它也迅速的占据了主导地位,即使是在微软内部,也只有极少数的不属于Word和Excel项目组的程序员明白它们所犯的错误。

暴乱就这样发生了。一群一直都搞不懂匈牙利命名法的程序员发现它们用的是既讨厌又没有卵用的错误版本时,他们开始反抗了。在系统型匈牙利语中仍保留着一些帮你发现错误的好的规则,如果你使用系统型匈牙利语,你可以在你使用变量的时候知道它的类型。但它的价值远不及应用型匈牙利语。

反抗在.NET第一次发布时达到了顶峰。最后微软开始告诉人们“匈牙利命名法是不被推荐使用的”。欢声雷动啊。我想他们都懒的说为什么。他们只是通过在命名法则的每个章节开始加上这样一句话“不要使用匈牙利命名法”。匈牙利命名法在当时并不受欢迎,所以没有人为此而抱怨。在Excel和World项目组之外的所有人都终于摆脱了这尴尬命名方式,他们人在在当前强类型检查和智能提示面前并不需要这劳什子。

但是应用型匈牙利语还是非常有价值的,它增强了代码的排列,让代码的读写、调试和维护都变得更简单了,这使得错误代码更容易被发现。

Before we go, there’s one more thing I promised to do, which is to bash exceptions one more time. The last time I did that I got in a lot of trouble. In an off-the-cuff remark on the Joel on Software homepage, I wrote that I don’t like exceptions because they are, effectively, an invisible goto, which, I reasoned, is even worse than a goto you can see. Of course millions of people jumped down my throat. The only person in the world who leapt to my defense was, of course, Raymond Chen, who is, by the way, the best programmer in the world, so that has to say something, right?

Here’s the thing with exceptions, in the context of this article. Your eyes learn to see wrong things, as long as there is something to see, and this prevents bugs. In order to make code really, really robust, when you code-review it, you need to have coding conventions that allow collocation. In other words, the more information about what code is doing is located right in front of your eyes, the better a job you’ll do at finding the mistakes. When you have code that says

dosomething();
cleanup();

… your eyes tell you, what’s wrong with that? We always clean up! But the possibility that dosomething might throw an exception means that cleanupmight not get called. And that’s easily fixable, using finally or whatnot, but that’s not my point: my point is that the only way to know that cleanup is definitely called is to investigate the entire call tree of dosomething to see if there’s anything in there, anywhere, which can throw an exception, and that’s ok, and there are things like checked exceptions to make it less painful, but the real point is that exceptions eliminate collocation. You have to look somewhere else to answer a question of whether code is doing the right thing, so you’re not able to take advantage of your eye’s built-in ability to learn to see wrong code, because there’s nothing to see.

现在我必须要再做一件事,那就是再次批评异常处理,上一次做这件事给我带来了很多麻烦。我在Joel on Software的论坛首页的一个即兴的评论里说,我不喜欢异常处理,因为异常处理实际上就是隐藏的goto,我认为这比显式的goto更加糟糕。当然很多人跳出来怼我。唯一一个站出来支持我的人是Raymond Chen,作为世界上最好的程序员,他当然要说点什么了,对吧?

现在来说一说异常处理。你的眼睛学会了如何去发现错误,这样就可以阻止bug的发生。为了让代码更加的健壮,在检阅代码时你就需要一套代码规范。换句话说,你眼前能够看到的信息越多,你就能够更容易的发现错误。当你看到下面的代码时

dosomething();
cleanup();

你的眼睛能告诉你它有什么错误吗?我们已经执行了清理。但是,dosomething方法可能抛出了一个异常,这就意味着cleanup方法并没有执行。这是很容易修复的,使用finally或者whatnot,我不这样认为。我的观点是,能够确保cleanup方法必然执行的方式,是你必须要知道dosomething方法里面执行的所有内容,无论在什么地方抛出一个异常,这也还行,因为你可以通过像检查所有异常这样的方式让这件事不那么痛苦,但最关键的是异常将信息拆散了。你必须要去看其他的地方才能确定程序是否能够正确运行,所以你将不再能够通过眼睛的优势就发现存在的问题。因为你什么都看不到。

Now, when I’m writing a dinky script to gather up a bunch of data and print it once a day, heck yeah, exceptions are great. I like nothing more than to ignore all possible wrong things that can happen and just wrap up the whole damn program in a big ol’ try/catch that emails me if anything ever goes wrong. Exceptions are fine for quick-and-dirty code, for scripts, and for code that is neither mission critical nor life-sustaining. But if you’re writing an operating system, or a nuclear power plant, or the software to control a high speed circular saw used in open heart surgery, exceptions are extremely dangerous.

I know people will assume that I’m a lame programmer for failing to understand exceptions properly and failing to understand all the ways they can improve my life if only I was willing to let exceptions into my heart, but, too bad. The way to write really reliable code is to try to use simple tools that take into account typical human frailty, not complex tools with hidden side effects and leaky abstractions that assume an infallible programmer.

当我只是写一个小脚本用来每天收集数据并且打印出来,异常处理就很棒了。我只想忽略全部潜在的问题,最好是把整个项目都用try/catch包起来,如果出了问题就给我发个邮件。异常处理对于应急写的代码,脚本,还有无关紧要的东西来说,还是很不错的。但是如果你在写操作系统的时候,或者控制心脏手术的告诉电锯程序中时,异常处理就会变的非常危险。

我知道人们会认为我是个不咋地的程序员,因为我无法正确的理解异常处理,如果我不能从心里接收异常处理,我就不能领会它给我带来的 提升,这太糟糕了。确保写出健壮代码的方式,应该是使用那些考虑到人类固有弱点的简单的工具,而不是认为程序员永远不会犯错的隐藏了有漏洞的抽象的复杂工具。

(全文完)

让错误代码更明显-Making Wrong Code Look Wrong相关推荐

  1. Pycharm 错误代码 Process finished with exit code 0

    错误代码:Process finished with exit code 0 1.问题描述 PyCharm正常运行,但没有得到预期的效果 2.解决办法 看运行的py文件是否有主函数,或者同一个工程文件 ...

  2. microsoft WINDOWS 系统错误代码

    microsoft WINDOWS 系统错误代码 MS Windows Error Messages Code Error Message 0  操作成功完成.   1  功能错误.   2  系统找 ...

  3. Google是如何做Code Review的?| CSDN原力计划

    作者 | 帅昕 xindoo 编辑 | 屠敏 出品 | CSDN 博客 我和几个小伙伴一起翻译了Google前一段时间放出来的Google's Engineering Practices docume ...

  4. 你只使用到了 VS Code 20% 的功能?让 VS Code 首著作者带你玩转 VS Code!

    Visual Studio Code 作为广受好评的开发工具,已经被越来越多的开发者当作首选的开发工具.然而,你真的了解 VS Code 了吗?你真的会使用 VS Code,把 VS Code 的强大 ...

  5. 你只使用到了 VS Code 20% 的功能?听听 VS Code 首著作者怎么说

    Visual Studio Code 作为广受好评的开发工具,已经被越来越多的开发者当作首选的开发工具.然而,你真的了解 VS Code 了吗?你真的会使用 VS Code,把 VS Code 的强大 ...

  6. 你也许只使用到了 VS Code 20% 的功能

    Visual Studio Code 作为广受好评的开发工具,已经被越来越多的开发者当作首选的开发工具.然而,你真的了解 VS Code 了吗?你真的会使用 VS Code,把 VS Code 的强大 ...

  7. 工欲善其事,必先利其器。如何玩转 VS Code?

    Visual Studio Code 作为广受好评的开发工具,已经被越来越多的开发者当作首选的开发工具.然而,你真的了解 VS Code 了吗?你真的会使用 VS Code,把 VS Code 的强大 ...

  8. VS Code上也能玩转Jupyter Notebook,这是一份完整教程

    关注上方"深度学习技术前沿",选择"星标公众号", 资源干货,第一时间送达! 本文转自:机器之心 自从 2019 年 VS Code Python 插件更新以后 ...

  9. Pycharm debug出现Qt 错误 Process finished with exit code -1073741819 (0xC0000005)

    使用pycharm debug的时候出现 This application failed to start because it could not find or load the Qt platf ...

最新文章

  1. Kubeedge Edged概述
  2. Linkedin 工程师如何优化他们的 Java 代码
  3. Linux 部署ftp报530 错误解决方案
  4. Spring事务原理(1),区区一个SpringBoot问题就被干趴下了
  5. vue基础整理-组件
  6. 【Vegas原创】GridView设定DataFormatString属性失效的解决方法
  7. 使用镜像源安装EASY_INSTALL和PIP教程
  8. 微信JSAPI支付,报错当前页面的URL未注册
  9. Adobe 2022软件安装错误代码107解决办法
  10. git 获取最新代码_程序员必知:这是一份全面 amp; 详细的 Git与Github 介绍指南
  11. 三角形周长最短问题_1.八年级数学:DE平分ABC的周长?怎么求DE的长?你想了很久吧?...
  12. java版spring cloud+spring boot+redis社交电子商务平台-docker-feign配置(五)
  13. 主动轮廓模型snake
  14. 【个人喜好诗词之一】前赤壁赋
  15. Ubuntu 编译XCB源码
  16. Ural 2045 Richness of words
  17. 我的世界java版旁观模式_我的世界:8个被判定为bug的特性,旁观模式:这锅让我来背...
  18. 庆祝鸿蒙指的是哪个生肖,12月中头彩,苦难转幸福,3生肖,鸿蒙紫气,运走上坡路,想啥就有啥...
  19. 百威啤酒,嬴彻自动驾驶卡车送
  20. 4款好用的密码管理器,你值得拥有

热门文章

  1. 【数学建模/数据分析论文写作】图表制作 | 数据可视化常用工具整理
  2. 电脑双屏 鼠标只能从屏幕的左边界移到另一个桌面,如何让鼠标从屏幕的右边界移到另一个桌面
  3. 隐藏网站后缀名.aspx
  4. 使用CTP API接口交易期货股票期权国债全市场品种
  5. QQ音乐的动效歌词是如何实践的? 1
  6. 格式工厂--转换视频格式
  7. 前端面试—网站性能优化
  8. echarts5.0新特性
  9. 高通骁龙820A的硬件模块部分简介
  10. 高通骁龙820A芯片,众多品牌为其站台