再论返回值

再论返回值

相关阅读
初问返回值
C++吐槽

最终,还是没能学会C++。但一点也不懊丧,甚至有些窃喜。😏


先看几个函数原型

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node, const char *service,
               const struct addrinfo *hints,
               struct addrinfo **res);

man7

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  __drv_aliasesMem LPVOID lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

win32

public sealed class OpenFileDialog : System.Windows.Forms.FileDialog

dotnet

func fileExists(atPath path: String, 
    isDirectory: UnsafeMutablePointer<ObjCBool>?) -> Bool

foundation OC

func downloadTask(with request: URLRequest, 
completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask

Swift rules


返回什么?

长久以来,或说从正式开始学习写程序以来,头脑中一直萦绕着这样一个问题:函数返回值应该是什么?

到如今,经过反复思考和求证、阅读编程老手的代码不断揣度、问各路神通这个问题后,我大致有了如下的一些太长不看的结论:

  • 不同时期主流编程语言不同,每个时期有不同风格,没有普适的答案
    • 过去由C语言统治的时期,由于不支持异常处理和多返回值,所以多采用返回错误代码errno、通过指针修改传入参数地址处的数据结构来返回的方法
    • 在Java流行的年代,值类型逐渐变多,传统风格方兴未艾,单个多个返回值、修改实参、修改类成员、修改全局变量、抛异常等等大杂烩
    • 在多核多进程多线程并发的当代,强类型、编译时安全语言与值类型语言逐渐占据优势
  • 当代面向人类开发者尽量多用符合语义的单/多值类型返回与异常处理;面向机器无所谓
  • 用好Optional
  • 原先的参数返回与返回值返回的考量,变成了现在同步值返回与异步回调的取舍
  • 函数式编程的思想会卷土重来

第一阶段

如引用和附录所言,自己第一次正经学编程语言,是在大一时学的C语言程序设计课上。中学虽然用过一些脚本和VB6.0,但都只是玩票,对于数据结构和算法完全没有认知。学完本科的C语言课程后,在很长一段时间里,我的编程思维都是面向过程的、C风格的,没法写出任何稍微复杂的需要有面向对象思维的程序。就拿本科写的最复杂的几个程序来说:MATLAB计算波导磁场、Verilog调制解调、LabVIEW信号采集。它们大多数都像是将数学公式和函数,或是一个操作过程用程序语言描述出来,只需要简单的control flow即可实现“自动化”。

那时非常常见的函数原型:

void foo();

完全就是用一个函数把一坨逻辑打包装起来,在需要的时候调用罢了。现在看来,这种所有语句全都堆在main.c里面的程序,完全可以用goto甚至汇编的je来实现。那时对于返回值基本相当于没有概念。

第二阶段

2017年9月第一次学正经的“工业语言”C++。说是正经学,实际上一知半解。对于面向对象的“封装继承多态”完全没有概念,重载和重写傻傻分不清。甚至连函数指针都唯恐避之不及。最后结果也相当凄惨,在献祭了人生第一次通宵后依旧备受打击。

对于工程、编程乃至工业的方方面面,我一直以为,如果有一个东西普通人学不会,那一定是它设计存在缺陷。C++便是这样一件事物。学习编程,应该是学习编程的思想,而不是在学习语言的语法。如果一个语言本身能够成为一种“技术壁垒”,或许它的未来会像许多前辈如COBOL、Fortran一般。

编程在21世纪会是泛在的。所有具有基础读写能力的年轻人都应该能够编程,至少能够输入逻辑让计算机完成特定的任务。当然,这些和返回值没啥关系。

写C++时对返回值更加疑惑了。写多态运算符重载时用的那一套符号让本就近视的我雪上加霜。事情本质很简单也很清晰,又为什么要用一堆非得按shift的符号来描述呢?从C过渡到C++时,逐渐产生了返回值的疑问。那时接触的C语言程序都不复杂,直接返回值,返回个指针或者不返回都很正常。但C++能返回实例,传参又能传引用,一下子就把我搞蒙了。程序一次跑过不难,valgrind一次不红可是难上加难。实话实说,到现在我也基本不理解RAII的用法,更别提写出对于细粒度内存管理完全自信的程序了。嗨,这些玩意,现在看来,难道不该让计算机自己完成吗?

川当时用Java刷题近300道,第二次学C++,551期末考了A+。他的一句话让我记忆犹新:

都2017年了,谁™还用指针啊。

大家会心一笑。后来我才知道Java连malloc都不用,更别提指针了。当然这些都是后话。

第三阶段

2019年6月的某一天,是我视作正式“转码”的日子——那天,我打开LeetCode,做了第一道题:Two Sum。第一次提交的代码——

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> result;
        int index1 = 0;
        int index2 = 1;
        std::vector<int>::iterator it1 = nums.begin();
        std::vector<int>::iterator it2 = nums.begin() + 1;
        while (it1 != nums.end()) {
            while(it2 != nums.end()) {
                if (*it1 + *it2 == target) {
                    result.push_back(index1); 
                    result.push_back(index2); 
                    return result;
                }
                else {
                    ++it2;
                    ++index2;
                }
            }
            ++it1;++index1;
            it2 = it1 + 1; // reset the position
            index2 = index1+1;
        }
    };
};

虽然一个五六行就能搞定的代码写了近30行,但那已经是我当时C++的最好水平了。有一段时间,自己在各种风格之间摇摆不定,从这个文件中可见一斑。缩进用tab还是空格、C++四格缩进、动词命令式函数名、名词函数名、“残疾”函数原型、C++和C风格注释混用。虽然难受的一屁,但“臣妾实在做不到啊”!那时自己缺少整体的编程素养,注定是写不出健壮的代码的。但就像之前所说,“赛跑”逼迫着转码选手“赶鸭子上架”。很多朋友就是靠着Java刷题顺利进入了心仪的公司。而且我相信这些大公司一定也有代码规范能让他们迅速成长起来,开发出风格统一的代码来。因此,当时还不如放下纠结无脑随大溜呢!

有一段时间“能不返回就不返回”,尽可能地遵照C风格的返回错误代码,指针传递结果的写法来写。较复杂的函数还好,一些简单的函数如strip(),都这样搞实在费劲。看着函数里一排指针就头疼!写C++后则遵照能不在子函数new就不new的写法来写,在main函数里把所有需要的内存全申请了,这总该可以了吧——引用传来传去还是头疼!后来只好随心所欲,遵循“想返回什么就返回什么”的写法。虽然写起来自然,但重看重构代码有时觉得以前的设计不好,也很纠结。没法子,上论坛发帖,结果发现这个问题确实没有定论。唉,还是得自己慢慢摸索!

再看上面的函数

比较熟悉的几种编译时语言的一些常用系统函数。反映了我的心路历程。

  • 开始写稍微复杂的C程序时,光是读man page就要读到头痛。无数的自定义结构体、传来传去的指针、未定义的返回错误代码。再加上字符串……还是只让C做它的本职工作好了。
  • 写奇怪的平台限定C++程序时:啥啥啥,这些玩意都是啥?如果MATLAB程序的库函数返回一个“handle”、句柄,大部分时候权当做空气无视掉。返回值看不懂不如看看参数。这些参数又是啥?没一个基础值类型,也不是标准库的数据结构。不如再来个LPCWSTR const wchar_t*图一乐。
  • C#终于回到了人类世界。真的,第一次写C#程序时,虽然完全不懂它的语法,但还是莫名地有了被微软温柔以待的感觉。风格沿袭C语言,而变得简明。不再纠结写点还是箭头,自动补全也比Sublime手撸C++畅快了许多。虽然文本编辑器也能补全,但预置没法一键查文档,查调用,还是比不上“宇宙第一”IDE来得方便。
  • Swift终于让我确信自己不是智障,甚至有了些自信。清晰、简明、一眼看去便知函数在做什么;返回值、参数、异步调用赫然在目,寥寥数语便能安全地把远隔重洋的数据下载到本地。这种对于程序运行结果的完全掌控感,是其他语言难以简单获得的。

或许下一步该看函数式语言了。

瞎比拟

  • C++像清真寺、Swift像麦当劳

C++是一种宗教,g++和MSVC++就像mantis与crystal;98和11是两门语言;17和21语法甚至让11的程序员看不懂。多像旧约新约和后期圣徒呀。

Swift则不然。流行、方便、普世、干净整洁标准化、质量中上。就像快餐品牌麦当劳。走遍全世界,或许有细微的差异,但大体上体验是相同的。这种可预知性,在战火纷飞瘟疫横行的年代,是多么难能可贵呀!走进了Swift的店面,那些叫嚷着追击的memory leak和race condition僵尸,被安全地阻挡在防弹玻璃门之外。

  • 自由与专制:从C到Java

C是自由的语言。如果汇编语言的复杂度上限是WPS这类软件的话,那C的海洋则是无限宽广。libc的泛在程度,或许比我眼前所见的任何一件事物都要高出几个数量级。

Java chaos

没有用过Java,在可预见的未来不会用Java。这里放一张图代表我的观点。

  • 设计者 vs 开发者

喜欢Swift的另一个原因:自己更多以设计者而非开发者自居。小时候最想当architect;长大后发现,虽然自己很难成长为优秀的设计师,但在生活中始终保有设计感、对设计保持敏锐的观察力非常重要。

Swift令设计人员摆脱程序编写的困扰。极其方便且安全地验证设计是否合适,而不必纠结细节;丰富、优秀且引领潮流的交互设计让创意与实现的距离不再遥远。

  • 程序设计语言的未来

新一代程序设计语言是人们对PL理解的里程碑——多数问题应该由机器自己、编译器解决,而非让初级开发者如履薄冰、举步维艰。

程序语言本身不应该是开发的门槛。

程序语言应该是简洁、健壮、快速而安全的。

从这个角度来看,工作中自己将会长期使用Swift;希望学习的下一门语言是Rust。C#是好,却是微软限定的好。这个层面上,更信任工程师而非科学家开发出的语言。

现代工具非常爽。庆幸于从开始学代码就用有现代工具的语言,而非那些艰深的、无法开发统一“清洗”工具的语言。linter让我少走了很多很多弯路。CI和Git让我避免了拷贝”桌面备份.zip”到机房的尴尬。

  • 代码风格

虽然这里扯了不少,但关于这个话题总有说不完的唠叨。以后再说。

但愿自己编程营生的生涯不要太长;总能写出学生标答风格的代码:结构优美、不一定是最简洁one-liner、最高可读性、不让计算机运行最快、没有黑魔法。让审计者犹如阅读高考题目答案一般,四平八稳挑不出毛病,也读不出天赋的痕迹。

不让计算机运行最快:或许可以写一篇关于“缓慢程序主义”的文章。如果有一天人工智能真的反噬人类,那么至少我做出过小小的抵抗——在合理的时间复杂度下,尽可能多地牺牲性能以换取对人的友善程度,不仅限于可读性和安全的错误处理。让计算机可控地慢下来。

在C和Swift里,想快就快想慢就慢。这样的迟缓是可控的。但对于C++,始终没能掌握合理地使程序慢到特定节奏的方法。

  • 优越感?

Java和C++14-历史包袱太过沉重,是典型过度工程的结果。相比之下,C经历了风云变幻,却依旧历久弥新,实乃上乘。

除非特定任务或给足钱,否则坚决不碰Java。看看Minecraft的混乱吧 xxUtils

希望自己的编程维生的生涯,在必须触碰Java前结束。

?


附录:关于返回值的胡比聊天

- Ting > Do we ever use `@inout` in production? File related wrapper functions (such as this link) in both Cocoa and iOS Foundation sometimes deals with reference or pointers, probably due to legacy interface in OS 9 era :confused:. But this is gonna got "fixed" by newly introduced https://developer.apple.com/documentation/system in next Swift release :smirk_cat:. - markd the reason C / objc needs to do that is that you can't have more than one return value (can't stuff them into a tuple because the language doesn't have tuples). So fileExists(atPath:isDirectory:) is a direct wrapper around the objc version, which is returning both a Bool (does it exist) and another Bool (OBTW is this a directory) - Ting Actually thinking more on this topic, I recalled a longtime intriguing question back when I started to learn programming with C - what should a function return? Really like to hear some thoughts! :pray: When I learned C for the first time, I tend to go "intuitively", sth like `int add(int a, int b) { return a + b }` After I learned pointer I got to know `void swap(int * a, int * b) { // ... }` And when I later touched syscalls, I found lots of system level functions set return value as error numbers, rather than the result of a function. Some consumes a pointer to a special struct as argument and change it as the “result”. This makes me confused, and I’m a bit lost what to use. :dizzy_face: Is there a "rule of thumb" for what to use as return value? I’d like to learn from veterans that how do industries design APIs through the years. The 👆 ObjC API mixes return value with param pointers, whereas system level libs like socket related stuff mostly return errno. - markd back in the Olden Days, the pointer-related calls were much more popular, because of the way memory worked with function calls. if you had a big struct, a doThingWith(hundredByteStructure) would copy a hundred bytes to the stack. And if you had hundredByteStructure = doThingWith(hundredByteStructure), that's another hundred bytes copied - markd so you'd use pointers doThingWith(&hundredByteStructure), and you're only pushing four bytes on to the stack, with a slight runtime penalty of indirecting through the pointer - markd and good observation of the calls that return errno - to return anything else, you have to pass something in by reference, which requires pointering it too. white_check_markeyesraised_hands - markd Unix APIs typically do the errno thing. Sockets is very weird. It does object-oriented style polymorphism via opaque pointers white_check_markeyesraised_hands - markd but for the most part, objc / Cocoa / Apple calls return a value. Some classes of calls that need an extra error return will take an NSError by pointer to fill in. Letting you ignore the error if you choose. Swift's way of interfacing with those are pretty elegant - Mikey Ward (BNR) What he said. :sweat_smile: Mark’s a trustable resource on that stuff. But yeah, when an ObjC method wanted to give you back two pieces of data, usually either a boolean or status code indicating success failure, and a data structure containing the body of output, the method would usually return the bool or int code, since that’s small data, and use its out parameter to give you a handle to the larger data structure. A cool side effect here is that it was returning to you information that you needed first - knowledge of where you had succeeded, or an error code - and then you could use that to decide whether to bother reading through the outparameter's pointer to read the larger payload. No sense in trying to read data you know won't be there, you know? - Mikey Ward (BNR) In Swift, however, it is generally more idiomatic to define a custom type (struct/class/enum) to encapsulate everything you want the caller to know about the results of the function call. We’ll see examples of doing this throughout the week. - Ting Thanks Mark and Mikey for the details. Quite informative! :smile: I guess part of the reasons we are moving towards a more value-based programming style are because - 1. computers are faster and memory is larger nowadays, which allows some overhead and 2. multithreading is less error-prone with each “task” having its own copy of data on stack. Is that correct? Powerful struct and enums in Swift are really big reasons for me to pick Swift as my go-to OOP language… :smirk_cat:
Chen Ting

Chen Ting

The page aimed to exhibit activities & achievements during Ting's undergraduate & graduate period. Meanwhile, other aspects such as lifestyles, literary work, travel notes, etc. would interweave in the narration.

Leave a Comment

Disqus might be GFW-ed. Excited!