404 发表于 2023-2-7 15:12:00

知乎 | 为什么有很多人执着于中文编程?

这个问题,前两天每天两万浏览。但它显然伤害了一些人的利益。于是两天后,它的浏览量马上被人为拉到0。
沉寂十几天后,大概利益相关方的水军已经到位,宣传计划制定妥当,于是限流期结束,浏览量回升。
真好。真是彻底体现了我们的优越性,证明了鼓吹中文编程的都是些什么人。
好吧,在此,我宣布,在下从今天起无条件支持中文编程,呼吁禁止在代码中使用英文标点。并对自己之前的认识不清道歉。原答案不改,就留在下面,以供有识之士们唾弃:
因为他们想忽悠那些不懂编程的人。
真懂了编程,你就知道,其实“编程语言”压根就是一门新语言——不是汉语、日语也不是德语、英语、世界语。
它是为计算机硬件、为开关电路量身定做的一套“符号系统”;这套“符号系统”定义了一堆类似数学符号的“关键字”,使得程序员可以像做数学题那样,把万事万物的数学骨架表达给电脑——最后,再通过“数字信息”到“位置”“色彩”“亮度”等东西的对应表,把算出来的东西转换成人类看得懂的图形、文本、表格之类形式。
如果你会做数学题,那么你一定不会抱怨什么“=不写成‘等于’”、“x不写成‘未知数’‘天元’”“+应该写成‘加’”……
你绝不敢说这些简化的数学符号妨碍了你的理解、使得你解不了数学题。
恰恰相反,不使用这种从格式开始就有明确区分、因此易写易读的数学符号,反而使得你的表述冗长、难以理解。
没人搞“中文数学”是因为方程这种东西是初中必修。大家都明白,所以没法忽悠人——说出来,就显得你傻。
编程是完全一样的。
几乎所有编程语言都支持三种控制结构,它们是:顺序、分支、循环。
顺序很简单,一行挨一行就行。
分支的助记符是if,if后面跟条件表达式,如果条件表达式计算出来为真,就走第一分支;否则走第二分支。写出来就是:
if (你中午没吃饭) {
去吃饭;
} else {
午休;
}
你当然也可以写成:
如果 (你中午没吃饭) {
去吃饭;
} 否则 {
午休;
}
类似的,各种编程语言里,常见的循环有四种:for循环、while循环、do-while/until循环和foreach循环。
for(int I=0; I<10; I++) {
}

while(p!=end) {
}

do {

} while(p!=end);

do {
} until(p==end)

foreach item in list {
}

你当然可以把它们也翻译成中文:
为(整型 循环变量=0; 循环变量<10; 循环变量 自增) {
}

当(指针 不等于 尾指针) {
}

做 {
} 当(指针 不等于 尾指针);

做 {
} 直到(指针 等于 尾指针);

为每个 元素 在 列表 {
}
多页面视图
当然,如果你懂编程,可能会觉得这个写法怪异。不过没关系,看多了自然就习惯了。
基于计算机工作原理,和数学一样,这套符号一共也就三四十或者五六十个(有的语言多一些,有的少一些)。它们就是所谓的“关键字”。
每个关键字,都代表着一种指令/属性/指示。
程序员必须像熟悉数学运算符/速记符那样,熟悉所有这些指令/属性/指示;然后组合它们,把希望计算机执行的操作无歧义的清晰表达出来。
嗯,我不打算讨论中文编程键入多不多、全角半角符号混淆起来有多烦人、中文空格和英文空格、制表符混用有多恶心。
这些都是能克服的。
事实上,只要我们在C里面导入一个头文件:
//中文关键字定义
//我懒,没写全。但C语言一共也只有32个关键字,你把它们以类似格式define一下就行。
//我都做1/3强了,你补上剩下2/3就行。汉化它就这么容易。

#define 若 if
#define 否则 else
#define 为 for
#define 当 while
#define 做 do
#define 整型 int
#define 字符 char
#define 短整 short
#define 单精数 float
#define 双精数 double
#define 结构体 struct 多页面视图
那么,C语言现在已经支持中文关键字了。
中文编程语言已经有了。
类似的,你也可以如此对待C++。
其他编程语言或许没有宏,做不到如此轻易,但写个程序做个字符串替换,这是初中生都能办到的。
哪怕现在最最复杂的语言也不过几十个关键字而已——前面我说过了,关键字就对应于数学符号,和+-×÷=、Σ、lim、|x|、∫等等都是一回事(数学符号往往还比编程语言关键字多得多,比如c语言据说就是32个关键字、48个运算符:运算符就是数学符号)。
总之,或者你利用宏,或者直接写个转换程序——这个程序完全可以是通用的,定义个转换表就能对付所有语言,就好像uedit/notepad++的语法提示那样,简单的识别-替换就足够用了——任何编程语言都能当即变成“中文编程语言”。
这事的难度就有这么低。
按照IT界一贯的尿性,如果这种东西真那么好用、那么有用的话,那么你到github上随便一搜,一定能得到一大堆的“中文编程头文件”“中文程序转换器”。
你看,只要你把C语言的32个关键字翻译一下,写到头文件里,github一丢,轻轻松松拿到几十几百万个star。
30分钟拿百万github star,这事真是想想都能笑醒。
但为什么就没人做这事呢?
太简单了。
俩字:扯淡。
就好像做数学题你不写Σ写求和、不写lim写极限、不写ln写以e为底的对数一样,纯属扯淡。
你不仅是为自己找别扭,你也是为所有看你的证明的人找别扭。
这一套符号,本就是键盘输入最简单、区分度最高又最为约定俗成的一套“速记符”;替换它不仅毫无意义,反而带来了输入、阅读、记忆诸方面的许多负担。
尤其是,诸如指针的指针、类A里面的类B对象的方法C返回的数据结构D里面的元素E这样的东西,拿“速记符”写,轻巧短小,一目了然;拿其他任何东西写……
只要真的接受过几天正规的编程教育,你就知道这东西纯属扯淡。
就好像只要上了初中,你就一定会写 4x+3y=20、就绝不会支持“四乘天元加三乘地元等于二十”这种写法一样。
因此,不会有人把这种东西丢上github,丢上去也不会有人关注它——除非故意恶搞。
反过来说也对:那些最为大力支持中文编程的人,也不会写这么个头文件出来。
因为这种东西,尽管吹、别落地,它看起来就是高大上的——越玄乎越好。
一旦落地,其中的荒谬滑稽,就透了出来。
嘴炮结束。现在让我们回归传统吧——talk is cheap, show me the code!
这是一段实际项目中的源代码。当然,我“鸡贼”的改了函数名,免得某些滥竽充数的家伙直接搜一些结论蒙混过关。
当然,这是段非常简单的代码。只要你的数据结构能学会六成以上,这是什么算法,你大概已经心中有数了。
嘘~~
别出声!
和我一起调戏调戏那些“中文编程党”吧【坏笑】:
void __cdecl SplitSort (
    void *base,
    size_t num,
    size_t width,
    int (__cdecl *comp)(const void *, const void *)
    )

{
    char *lo, *hi;            /* ends of sub-array currently sorting */
    char *mid;                  /* points to middle of subarray */
    char *loguy, *higuy;      /* traveling pointers for partition step */
    size_t size;                /* size of the sub-array */
    char *lostk, *histk;
    int stkptr;               /* stack for saving sub-array to be processed*/

    if (num < 2 || width == 0)
      return;               /* nothing to do */

    stkptr = 0;               /* initialize stack */
    lo = base;
    hi = (char *)base + width * (num-1);      /* initialize limits */

recurse:
    size = (hi - lo) / width + 1;      /* number of el's to sort */

    if (size <= CUTOFF) {
      shortsort(lo, hi, width, comp);
    } else {
      mid = lo + (size / 2) * width;      /* find middle element */

      /* Sort the first, middle, last elements into order */
      if (comp(lo, mid) > 0) {
            swap(lo, mid, width);
      }

      if (comp(lo, hi) > 0) {
            swap(lo, hi, width);
      }

      if (comp(mid, hi) > 0) {
            swap(mid, hi, width);
      }

      loguy = lo; /* traveling pointers for partition step*/

      higuy = hi; /* traveling pointers for partition step*/



      /* Note that higuy decreases and loguy increases on every iteration,

         so loop must terminate. */

      for (;;) {
            if (mid > loguy) {
                do{
                  loguy += width;
                } while (loguy < mid && comp(loguy, mid) <= 0);
            }

            if (mid <= loguy) {
                do{
                  loguy += width;
                } while (loguy <= hi && comp(loguy, mid) <= 0);
            }

            do{
                higuy -= width;
            } while (higuy > mid && comp(higuy, mid) > 0);

            if (higuy < loguy)
                break;

            swap(loguy, higuy, width);

            /* If the partition element was moved, follow it.Only need
               to check for mid == higuy, since before the swap,
               A > A implies loguy != mid. */
            if (mid == higuy)
                mid = loguy;

             /* A <= A, A > A; so condition at top
               of loop is re-established */
      }

      /*   A <= A for lo <= i < loguy,
               A > A for higuy < i < hi,
               A >= A
               higuy < loguy
         implying:
               higuy == loguy-1
               or higuy == hi - 1, loguy == hi + 1, A == A */


      /* Find adjacent elements equal to the partition element.The
         doubled loop is to avoid calling comp(mid,mid), since some
         existing comparison funcs don't work when passed the same value
         for both pointers. */
      higuy += width;
      if (mid < higuy) {
            do{
                higuy -= width;
            } while (higuy > mid && comp(higuy, mid) == 0);
      }

      if (mid >= higuy) {
            do{
                higuy -= width;
            } while (higuy > lo && comp(higuy, mid) == 0);
      }

      /* OK, now we have the following:
            higuy < loguy
            lo <= higuy <= hi
            A<= A for lo <= i <= higuy
            A== A for higuy < i < loguy
            A>A for loguy <= i < hi
            A >= A */



       /* We've finished the partition, now we want to sort the subarrays
          and .
         We do the smaller one first to minimize stack usage.
         We only sort arrays of length 2 or more.*/

      if ( higuy - lo >= hi - loguy ) {
            if (lo < higuy) {
                lostk = lo;
                histk = higuy;
                ++stkptr;
            }                           /* save big recursion for later */

            if (loguy < hi) {
                lo = loguy;
                goto recurse;         /* do small recursion */
            }
      }
      else {
            if (loguy < hi) {
                lostk = loguy;
                histk = hi;
                ++stkptr;               /* save big recursion for later */
            }

            if (lo < higuy) {
                hi = higuy;
                goto recurse;         /* do small recursion */
            }
      }
    }

    /* We have sorted the array, except for any pending sorts on the stack.
       Check if there are any, and do them. */

    --stkptr;
    if (stkptr >= 0) {
      lo = lostk;
      hi = histk;
      goto recurse;         /* pop subarray from stack */
    }
    else
      return;               /* all subarrays done */
} 多页面视图
现在,我们汉化它的关键字,看看能不能降低理解难度:
空 函数调用模式乙 SplitSort (
    空 *base,
    尺寸类型 num,
    尺寸类型 width,
    整型 (函数调用模式乙 *comp)(不变的 空 *, 不变的 空 *)
    )

{
    字符 *lo, *hi;            /* ends of sub-array currently sorting */
    字符 *mid;                  /* points to middle of subarray */
    字符 *loguy, *higuy;      /* traveling pointers for partition step */
    尺寸类型 size;                /* size of the sub-array */
    字符 *lostk, *histk;
    整型 stkptr;               /* stack for saving sub-array to be processed*/

    若 (num < 2 || width == 0)
      返回;               /* nothing to do */

    stkptr = 0;               /* initialize stack */
    lo = base;
    hi = (char *)base + width * (num-1);      /* initialize limits */

recurse:
    size = (hi - lo) / width + 1;      /* number of el's to sort */

    若 (size <= CUTOFF) {
      shortsort(lo, hi, width, comp);
    } 否则 {
      mid = lo + (size / 2) * width;      /* find middle element */

      /* Sort the first, middle, last elements into order */
      若 (comp(lo, mid) > 0) {
            swap(lo, mid, width);
      }

      若 (comp(lo, hi) > 0) {
            swap(lo, hi, width);
      }

      若 (comp(mid, hi) > 0) {
            swap(mid, hi, width);
      }

      loguy = lo; /* traveling pointers for partition step*/

      higuy = hi; /* traveling pointers for partition step*/



      /* Note that higuy decreases and loguy increases on every iteration,

         so loop must terminate. */

      为 (;;) {
            若 (mid > loguy) {
                做{
                  loguy += width;
                } 当 (loguy < mid && comp(loguy, mid) <= 0);
            }

            若 (mid <= loguy) {
                做{
                  loguy += width;
                } 当 (loguy <= hi && comp(loguy, mid) <= 0);
            }

            做{
                higuy -= width;
            } 当 (higuy > mid && comp(higuy, mid) > 0);

            若 (higuy < loguy)
                中断;

            swap(loguy, higuy, width);

            /* If the partition element was moved, follow it.Only need
               to check for mid == higuy, since before the swap,
               A > A implies loguy != mid. */
            若 (mid == higuy)
                mid = loguy;

             /* A <= A, A > A; so condition at top
               of loop is re-established */
      }

      /*   A <= A for lo <= i < loguy,
               A > A for higuy < i < hi,
               A >= A
               higuy < loguy
         implying:
               higuy == loguy-1
               or higuy == hi - 1, loguy == hi + 1, A == A */


      /* Find adjacent elements equal to the partition element.The
         doubled loop is to avoid calling comp(mid,mid), since some
         existing comparison funcs don't work when passed the same value
         for both pointers. */
      higuy += width;
      若 (mid < higuy) {
            做{
                higuy -= width;
            } 当 (higuy > mid && comp(higuy, mid) == 0);
      }

      若 (mid >= higuy) {
            做{
                higuy -= width;
            } 当 (higuy > lo && comp(higuy, mid) == 0);
      }

      /* OK, now we have the following:
            higuy < loguy
            lo <= higuy <= hi
            A<= A for lo <= i <= higuy
            A== A for higuy < i < loguy
            A>A for loguy <= i < hi
            A >= A */



       /* We've finished the partition, now we want to sort the subarrays
          and .
         We do the smaller one first to minimize stack usage.
         We only sort arrays of length 2 or more.*/

      若 ( higuy - lo >= hi - loguy ) {
            若 (lo < higuy) {
                lostk = lo;
                histk = higuy;
                ++stkptr;
            }                           /* save big recursion for later */

            若 (loguy < hi) {
                lo = loguy;
                去到 recurse;         /* do small recursion */
            }
      } 否则 {
            若 (loguy < hi) {
                lostk = loguy;
                histk = hi;
                ++stkptr;               /* save big recursion for later */
            }

            若 (lo < higuy) {
                hi = higuy;
                去到 recurse;         /* do small recursion */
            }
      }
    }

    /* We have sorted the array, except for any pending sorts on the stack.
       Check if there are any, and do them. */

    --stkptr;
    若 (stkptr >= 0) {
      lo = lostk;
      hi = histk;
      去到 recurse;         /* pop subarray from stack */
    }
    否则
      返回;               /* all subarrays done */
} 多页面视图
看懂了吗?
是不是好懂多了?
真懂了?
那么,回答如下问题:
1、这个排序算法的复杂度级别是?
2、什么情况下,这个算法的复杂度会变成O(N^2)?
3、这个排序算法是稳定的吗?
还不懂?
那咱继续——现在,我们的目标是:不再有英文!
空 函数调用模式乙 分割排序(
    空 指针 基,
    尺寸类型 数字,
    尺寸类型 宽度,
    整型 (函数调用模式乙 指针 比较)(不变的 空 指针, 不变的 空 指针)
    )

{
    字符 指针 低, 指针 高;            /* 当前排序的子数组两头 */
    字符 指针 中;                  /* 指向子数组中间的指针 */
    字符 指针 低小伙, 指针 高小伙;      /* 分区步骤的行进指针 */
    尺寸类型尺寸;                /* 子数组的大小 */
    字符 指针 低栈[栈大小], 指针 高栈[栈大小];
    整型栈指示器;               /* 用于保存要处理的子数组的堆栈 */

    若 (数字 < 2 || 宽度 == 0)
      返回;               /* 不需要做任何事 */

   栈指示器 = 0;               /* 初始化栈 */
   低 = 基;
   高 = (char 指针)基 + 宽度 * (数字-1);      /* 初始化限制 */

递归:
   尺寸 = ( 高 -低) / 宽度 + 1;      /* 需要排序的元素数目(良心翻译啊,el我都译了!)*/

    若 ( 尺寸 <= 隔断) {
      短排序( 低,高, 宽度, 比较);
    } 否则 {
         中 =低 + ( 尺寸 / 2) * 宽度;      /* 找到中间元素 */

      /* 把起首、中间、后端元素排序*/
      若 (比较( 低,中) > 0) {
            交换( 低,中, 宽度);
      }

      若 (比较( 低,高) > 0) {
            交换( 低,高, 宽度);
      }

      若 (比较( 中,高) > 0) {
            交换( 中,高, 宽度);
      }

         低小伙 =低; /* 分区步骤的行进指针 */

         高小伙 =高; /* 分区步骤的行进指针 */



      /* 注意:在每一次迭代中,高小伙递减且低小伙递增,
         因此循环必然会终结。*/

      为 (;;) {
            若 ( 中 >低小伙) {
                做{
                     低小伙 += 宽度;
                } 当 ( 低小伙 <中 && 比较( 低小伙,中) <= 0);
            }

            若 ( 中 <=低小伙) {
                做{
                     低小伙 += 宽度;
                } 当 ( 低小伙 <=高 && 比较( 低小伙,中) <= 0);
            }

            做{
               高小伙 -= 宽度;
            } 当 ( 高小伙 >中 && 比较( 高小伙,中) > 0);

            若 ( 高小伙 <低小伙)
                中断;

            交换( 低小伙,高小伙, 宽度);

            /* 如果分界元素移动了, 跟随它.只需要检查中 ==高小伙, 因为在交换前,
               A[ 低小伙] > A[ 中] 意味着 低小伙 !=中. */
            若 ( 中 ==高小伙)
               中 =低小伙;

             /* A[ 低小伙] <= A[ 中], A[ 高小伙] > A[ 中]; 因此在循环之初,初始条件会重新建立 */
      }

      /*   A <= A[ 中] for低 <= i <低小伙,
               A > A[ 中] for高小伙 < i <高,
               A[ 高] >= A[ 中]
                高小伙 <低小伙
         意味着:
                高小伙 ==低小伙-1
               or高小伙 ==高 - 1,低小伙 ==高 + 1, A[ 高] == A[ 中] */


      /* 识别相邻元素和分区元素相同的情况。分成两个循环是为了避免调用 比较( 中, 中), 因为传入的两个指针指向同一位置时,某些比较函数无法正常工作。*/
         高小伙 += 宽度;
      若 ( 中 <高小伙) {
            做{
               高小伙 -= 宽度;
            } 当 ( 高小伙 >中 && 比较( 高小伙,中) == 0);
      }

      若 ( 中 >=高小伙) {
            做{
               高小伙 -= 宽度;
            } 当 ( 高小伙 >低 && 比较( 高小伙,中) == 0);
      }

      /* 好了,现在我们得到如下状态:
               高小伙 <低小伙
               低 <=高小伙 <=高
            A<= A[ 中] 当低 <= i <=高小伙
            A== A[ 中] 当高小伙 < i <低小伙
            A>A[ 中] 当低小伙 <= i <高
            A[ 高] >= A[ 中] */



       /* 我们已经结束了分割。现在我们希望排序子数组[ 低,高小伙] 和 [ 低小伙,高].
          我们先处理较小的那个数组,以尽量少的占用栈
          我们仅对长度大于等于2的数组排序。*/

      若 (高小伙 -低 >=高 -低小伙 ) {
            若 ( 低 <高小伙) {
               低栈[ 栈指示器] =低;
               高栈[ 栈指示器] =高小伙;
                ++ 栈指示器;
            }                           /* 为将来保存大递归 */

            若 ( 低小伙 <高) {
               低 =低小伙;
                去到 递归;         /* 执行小递归 */
            }
      } 否则 {
            若 ( 低小伙 <高) {
               低栈[ 栈指示器] =低小伙;
               高栈[ 栈指示器] =高;
                ++ 栈指示器;               /* 为将来保存大递归 */
            }

            若 ( 低 <高小伙) {
               高 =高小伙;
                去到 递归;         /* 执行小递归 */
            }
      }
    }

    /* 我们已经排序了数组,但栈里保存的除外。
       检查一下栈里是否还有未排序数组,排序它。 */

    -- 栈指示器;
    若 ( 栈指示器 >= 0) {
         低 =低栈[ 栈指示器];
         高 =高栈[ 栈指示器];
      去到 递归;         /* 从栈里弹出子数组 */
    }
    否则
      返回;               /* 所有的子数组已经排序 */
} 多页面视图
这回看懂了吗?
有没有变得更好懂呢?
真懂了?
那么,回答如下问题:
1、这个排序算法的复杂度级别是?
2、什么情况下,这个算法的复杂度会变成O(N^2)?
3、这个排序算法是稳定的吗?
很显然,所谓“中文关键字/中文标识符就能让程序变得更好懂”完全是在扯淡。
如你所见,这个程序甚至还变得更难懂了。
为什么更难懂了?
1、缺乏合理的命名规范,整个程序命名太过随意,完全无法区分不同语言元素。
如果你熟悉C编码规范,通过大小写以及命名规范,很容易看出哪些是常量、哪些是变量、哪些是函数——通过规范的编码来说明程序功能、快速读懂一个陌生程序,这是程序员的基本功。
但缺乏规范和大小写区分的中文翻译抹杀了这些区别,把它变得更难懂了。
2、互联网上已经有海量的、关于这个算法的分析资料。
但一旦译成中文,因为资料的贫乏,你反而再也得不到任何帮助。
你的知识,再也不可能更新,再也不可能提高。
翻译者给你画多大个圈,你就只能呆在这个圈里面。翻译者忘了喂你,你就只能饿死。
3、真正理解这段程序,需要的并不是看这段代码。
恰恰相反,想看懂这段程序,课本上是整整一章的内容;加上其他相关知识,你起码要看4、50页书,这才能明白它的设计思路,这才能学会它。
而随意翻译断开了“具体代码”和“相关资料”之间的链接。
那么,这段代码,尤其是这段中文代码,如果单独拎出来的话,这个世界上恐怕都不会有人能看懂它。
因为只要我不说,就没人能把里面的指令、指示、数据类型、存储对象区分开。
那么,真正的程序员是怎么学、怎么写、怎么应付这类情况的呢?
很简单,做好索引,甚至干脆数字化。
比如说,编译器发现你的程序错了,它不仅会给你一串中文/英文提示信息,还会给你个C开头的四位数字。
所有这些数字都遵循同一个规范;因此你只需打开浏览器,把“C4996”输入,搜索,好了,问题解决。
类似的,链接器发现你的程序有问题,它也会给你个LNK开头的四位数字——这个数字同样遵循同一个规范。
类似的,MySQL、Windows、Linux,它们报的所有错误,也都会带一个唯一的错误码。
到网上搜这个错误码,你就你能得到你的母语写的、铺天盖地的心得体会、排错经验。
类似的,各种语言、各种框架/库,它们往往也会用相关的接口名称作为关键字,帮你索引相关文章。
比如,建立socket时,ErrorCode等于394是什么意思?
你看,socket、ErrorCode、394,这三个关键字就能锁定问题。
替换了其中任意一个,你就慢慢头疼去吧。
所以,你看,把现有代码替换成中文真的很蠢。
它彻底的断绝了你求助的可能,杜绝了你进步的可能——因此,对中文编程鼓吹者,有一个算一个,肯定没一个懂编程。
当然,在程序里使用中文仍然是可行的、有益的。比如说,国内程序员习惯用中文注释:
//经理说这里要慢一点,客户掏钱再给他优化
sleep(30000
本帖来自安卓秘书
页: [1]
查看完整版本: 知乎 | 为什么有很多人执着于中文编程?