记录一个 Mongoose 和 Koa 相性不和之处

return schema.save(function(err, sch){
  if(err){
    ctx.body = err;
  }else{
    ctx.body = "success";
  }
}).then(() => next());

这样写是无法正确对 ctx.body 赋值的,因为这个回调是 mongoose 自己的一个 callback,而 save() 返回的是一个空的 Promise,原因是传入了一个 callback。

所以实际的执行流程是这样的: save->空的promise->then->callback。

Mongoose 这里是传入一个 callback 或者直接使用,不要传 callback 进去。

正确做法如下:

  const resultPromise = new Promise((res,  rej) => {
    shcema.save(function(err, article){
      if(err){
        rej(err)
      }else{
        res(article)
      }
    })
  });
  await resultPromise.then((thing) => {
    ctx.body = thing;
    return next()
  }).catch((err) => {
    ctx.body = err;
  });

或者

  await shcema.save().then((thing) => {
    ctx.body = thing;
    return next()
  }).catch((err) => {
    ctx.body = err;
  });

具体原因在,…/mongoose/lib/services/model/applyHooks.js

return function wrappedPointCut() {
        var Promise = PromiseProvider.get();
​
        var _this = this;
        var args = [].slice.call(arguments);
        console.log({
          oldArgs: arguments
        })
        var lastArg = args.pop();
        var fn;
        var originalError = new Error();
        var $results;
        if (lastArg && typeof lastArg !== 'function') {
          args.push(lastArg);
        } else {
          fn = lastArg;
        }
​
        var promise = new Promise.ES6(function(resolve, reject) {
          args.push(function(error) {
            if (error) {
              // gh-2633: since VersionError is very generic, take the
              // stack trace of the original save() function call rather
              // than the async trace
              if (error instanceof VersionError) {
                error.stack = originalError.stack;
              }
              _this.$__handleReject(error);
              reject(error);
              return;
            }
​
            // There may be multiple results and promise libs other than
            // mpromise don't support passing multiple values to `resolve()`
            $results = Array.prototype.slice.call(arguments, 1);
            console.log({arguments})
            console.log({$results})
            resolve.apply(promise, $results);
          });
​
          _this[_newName].apply(_this, args);
        });
  
        // 存在 callback,因此执行了 callback,但是 koa 不认,所以修改就失效了。
        if (fn) {
          if (this.constructor.$wrapCallback) {
            fn = this.constructor.$wrapCallback(fn);
          }
          return promise.then(
            function() {
              process.nextTick(function() {
                fn.apply(null, [null].concat($results));
              });
            },
            function(error) {
              process.nextTick(function() {
                fn(error);
              });
            });
        }
        return promise;
      };

使用 JetBrains 注解库给你的 Java 代码带来一丝活力

依赖

  • annotations-java8.jar

这个包在任何一个 JetBrains IDE 的安装目录里面都有,在 kotlin-runtime.jar 里面也有,在 maven 仓库也有。

为什么需要它

首先我们知道 Kotlin 解决了一个万年大问题—— null 。 几乎每个代码量超过两千的项目,先做出原型后,前两次运行或单元测试,都死在 NullPointerException 上。 Kotlin 从根本上解决了这个问题——它要求你在编译期处理所有可能为 null 的情况,这是语言级别的支持,比C#那种只提供一个语法糖的半吊子好得多。

但是反观 Java ,就没这玩意。 因此,伟大的 JetBrains 就搞了个注解库,然后通过在 IDE 里面提示你处理那些可能为 null 的值(毕竟你没法直接让编译器自己检查并提示)来解决这个问题 (其实人家自己早就在用了,只是 Kotlin 也用到了这个注解库而已)。

我写的全限定名,因为把包名也写出来可以方便读者区分它和另外一套同名注解(sun的垃圾玩意)。

第一个注解, @TestOnly

 

@org.jetbrains.annotations.TestOnly

这个说都不用说了,就是专门写给单元测试服务的相关代码的注解。比如我的算法库,在单元测试端我写了对应的暴力算法来验证我的算法的正确性(这种方法在 OI 中称为对拍),那么这几个暴力算法的实现就适合使用 @TestOnly 注解修饰。

 

/**
 * brute force implementation of binary indexed tree.
 */
private inner class BruteForce
@TestOnly
internal constructor(length: Int) {
	@TestOnly
	private val data = LongArray(length)

	@TestOnly
	fun update(from: Int, to: Int, value: Long) {
		(from..to).forEach { data[it] += value }
	}

	@TestOnly
	fun query(from: Int, to: Int): Long {
		var ret = 0L
		(from..to).forEach { ret += data[it] }
		return ret
	}

	@TestOnly
	operator fun get(left: Int, right: Int) = query(left, right)
}

第二、三个注解, @NotNull 和 @Nullable

 

@org.jetbrains.annotations.NotNull
@org.jetbrains.annotations.Nullable

这个很简单,顾名思义。它们一般被用来注解带有返回值的方法、方法参数、类的成员变量。

当 @NotNull 注解一个方法参数的时候, IDE 会在调用处提示你处理 null 的情况(当然,如果 IDE 语义上认为你传进去的参数不可能是 Null ,那么当然没有提示了); 当它注解一个有返回值的方法的时候,它会检查返回值是否可能是 null 。如果可能,那也会有提示。

当 @Nullable 注解一个方法参数的时候, IDE 会在方法内部提示你处理该参数为 null 的情况; 当它注解一个有返回值的方法的时候,会在调用处提示你处理方法返回值为 null 的情况。

相应的,任何以 @NotNull 修饰的变量/属性/方法,它在 Kotlin 中对应的类型都写作它本身,即非 null 类型,任何以 @Nullable 修饰的变量/属性/方法,它在 Kotlin 中对应的类型都写作它本身后面跟一个问号,即可能为 null 的类型。 这和 Java 的区别在于, Kotlin 编译器强制你处理 null , Java 只有 IDE 警告。 另外,这个注解本身的命名也是一种警告,不过是对于开发者而言的。

第四、五个注解, @Nls 和 @NonNls

 

@org.jetbrains.annotations.Nls
@org.jetbrains.annotations.NonNls

这是两个十分有意思的注解,用于修饰字符串,而且是写给看的,这个就不是给 IDE 看的辣。 @Nls 用于修饰自然语言字符串,比如下面这个例子。 假设 textArea 是一个 JTextArea ,是一个游戏画面用于显示一些提示信息的框框。 这些提示信息当然是自然语言了。

所以就可以这样修饰:

 

public void boyNextDoor(@Nls String msg) {
	textArea.append(msg);
}

或者用于程序的 Crash 信息:

 

public DividedByZeroException(@NotNull @Nls String msg) {
	super(msg);
}

然后阅读代码的人就知道,哦,这个 msg 里面是一些自然语言,比如什么”My name is Van, I’m an artist.”之类的。

反之, @NonNls 就是用于修饰非自然语言。 比如我的算法库有一个大整数类,其中有一个构造方法接收一个字符串,然后这个大整数就从字符串构造。 这个字符串一般长这样: “23333333333333333333333333” 或者: “-6666666666666666666666666666666”。

这显然不是自然语言!于是:

 

public BigInt(@NotNull @NonNls String origin) {
	boolean sigTemp;
	if (origin.startsWith("-")) {
		sigTemp = false;
		origin = origin.substring(1);
	} else sigTemp = true;
	byte[] ori = origin.getBytes();
	if (ori.length == 1 && ori[0] == '0') sigTemp = true;
	data = ori;
	sig = sigTemp;
}

最后的注解 @Contract

 

@org.jetbrains.annotations.Contract

我已经在上一篇博客提到了这个神奇的 @Contract 注解。事实上,这个注解能描述的内容更详细,还能在一定程度上代替 @NotNull 和 @Nullable 。

这是一个有值的、用于修饰那些参数数量大于零并且返回值不为 void 的普通方法和构造方法的注解,比起 @NotNull 和 @Nullable ,它相对来说更复杂。因此,首先我们得看看它的源码。

 

package org.jetbrains.annotations;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface Contract {
	String value() default "";

	boolean pure() default false;
}

可以看到,这个注解类有两个属性。首先有一个 value 的字符串属性,还有一个 pure 的布尔属性。

首先从容易理解的 pure 说起吧。

pure 属性

这个属性听名字都能猜出什么意思——表示被注解的函数(就是方法,包含普通方法、静态方法和构造方法,下同)是否为纯函数。

纯函数(好像)是一个 fp 里面的概念,如果一个函数,对于特定的输入,都将产生对应的唯一的输出,并且不会影响别的东西(即没有副作用),那么这个函数就是纯函数。 数学上的函数就是最标准的纯函数,比如我有一个 f(x) ,那么对应每一个 x ,这个函数的输出都有一个对应的 f(x) ,并且我多次输入同一个 x ,输出的 f(x) 也是同一个。 这就是一个纯函数。

举个反例,下面是一段完整的(即可以编译运行的) C 语言代码,这里的函数 not_Pure 就不是一个纯函数。

 

#include <stdio.h>

int not_Pure(int assWeCan) {
	static int boyNextDoor = 233;
	return ++boyNextDoor * assWeCan;
}

int main(const int argc, const char *argv[]) {
	printf("%d\n", not_Pure(5));
	printf("%d\n", not_Pure(5));
	printf("%d\n", not_Pure(5));
	printf("%d\n", not_Pure(5));
	printf("%d\n", not_Pure(5));
	return 0;
}

我在 main 函数里面对 not_Pure 进行了五次调用,每次传进去的参数都是 5 ,但是它却产生了这样的输出:

 

1170
1175
1180
1185
1190

好。话题回到 @Contract 上。那么这个 pure 值怎么使用呢?

当你的函数是一个纯函数时(比如三角函数运算,这是再简单不过的纯函数了),你就可以这样修饰。

 

	@Contract(pure = true)
	public static native double sin(double a);

	@Contract(pure = true)
	public static native double cos(double a);

	@Contract(pure = true)
	public static native double tan(double a);

	@Contract(pure = true)
	public static native double cot(double a);

	@Contract(pure = true)
	public static native double csc(double a);

	@Contract(pure = true)
	public static native double sec(double a);

再比如我算法库大整数的 JNI 端和 Java 端,加减乘除和比大小都不会影响原来的两个算子,会新分配一块内存来放置运算结果,那么这些函数也统统可以使用 @Contract(pure = true)注解。

value 属性

这是 @Contract 注解的精髓,应用场景也非常好说明。 首先考虑一个函数——大整数类的 equals 。 我当然是需要处理一些特殊情况的,比如如果你传进去了一个 null ,那么我返回 true 。于是按照 Java 标准库那个注释思路,我们应该这样写:

 

/**
 * This is a pure function.
 * returns true if the value of given
 * parameter is equaled to the caller.
 *
 * if obj is null, it will return false.
 * if obj is not a org.algo4j.BigInt, it will return false.
 *
 * @param obj the given obj
 * @returns if obj is equaled to the caller
 */
@Override
public boolean equals(@Nullable Object obj) {
	if (obj == null || !(obj instanceof BigInt)) return false;
	if (obj == this) return true;
	return compareTo((BigInt) obj) == 0;
}

但是,仔细想想,其实你只是需要告诉用户,要是你给我 null ,我就还你 false 。 使用一大坨自然语言描述这个简单逻辑,不仅会让人看很久,而且如果注释丢了,用户也就无从得知这个情况了,而且这对于英语不好的人非常不友好。

于是我们可以这样写:

 

@Override
@Contract(value = "null -> false", pure = true)
public boolean equals(@Nullable Object obj) {
	if (obj == null || !(obj instanceof BigInt)) return false;
	if (obj == this) return true;
	return compareTo((BigInt) obj) == 0;
}

首先我们省去了一大堆注释,并且使用了一个字符串描述这个逻辑: “null -> false” 。意思就是,给我一个 null ,还你一个 false 。

这个字符串由两部分组成,箭头前面的是参数说明,后面是返回值说明。如果有多种需要说明的情况,那么使用分号隔开。

一共有这么几个允许使用的值:

 

null // null
!null // not null
true // boolean value true
false // boolean value falses
fail // means this function will not work in this case
_ // any value

一共六个,不要忽略了最后一个下划线,它的含义和 Scala 中的下划线相同——表示通配符。比如这个扩展欧几里得函数:

 

@NotNull
@Contract(value = "_, _ -> !null", pure = true)
public static ExgcdRes exgcd(long a, long b) {
	return new ExgcdRes(exgcdJni(a, b));
}

直接两个注解说明一切:首先返回值为非 null ,因此 @NotNull 。 然后,不论你传进来任何值,我都不会返回 null 的,因此 @Contract(“_, _ -> !null”) 。 最后,它是一个纯函数,因此 pure = true 。综上,有了这个函数的注解:

 

@NotNull
@Contract(value = "_, _ -> !null", pure = true)

我那几个大整数加减乘除的方法就可以这样写了:

本地接口:

 

@NotNull
@Contract(value = "!null, !null -> !null", pure = true)
private static native byte[] plus(@NotNull byte[] a, @NotNull byte[] b);

@NotNull
@Contract(value = "!null, !null -> !null", pure = true)
private static native byte[] times(@NotNull byte[] a, @NotNull byte[] b);

@NotNull
@Contract(value = "!null -> !null", pure = true)
private static native byte[] times10(@NotNull byte[] a);

@NotNull
@Contract(value = "!null, !null -> !null", pure = true)
private static native byte[] divide(@NotNull byte[] a, @NotNull byte[] b);

@NotNull
@Contract(value = "!null, !null -> !null", pure = true)
private static native byte[] minus(@NotNull byte[] a, @NotNull byte[] b);

@NotNull
@Contract(value = "!null, _ -> !null", pure = true)
private static native byte[] pow(@NotNull byte[] a, int pow);

@Contract(pure = true)
private static native int compareTo(@NotNull byte[] a, @NotNull byte[] b);

调用:

 

@NotNull
@Contract(value = "_ -> !null", pure = true)
public BigInt plus(@NotNull BigInt anotherBigInt) {
	if (sig == anotherBigInt.sig)
		return new BigInt(plus(data, anotherBigInt.data), sig);
	if (compareTo(data, anotherBigInt.data) > 0)
		return new BigInt(minus(data, anotherBigInt.data), sig);
	return new BigInt(minus(anotherBigInt.data, data), !sig);
}

@NotNull
@Contract(value = "_ -> !null", pure = true)
public BigInt minus(@NotNull BigInt anotherBigInt) {
	if (sig != anotherBigInt.sig)
		return new BigInt(plus(data, anotherBigInt.data), sig);
	if (compareTo(data, anotherBigInt.data) > 0)
		return new BigInt(minus(data, anotherBigInt.data), sig);
	return new BigInt(minus(anotherBigInt.data, data), !sig);
}

@NotNull
@Contract(value = "_ -> !null", pure = true)
public BigInt times(@NotNull BigInt anotherBigInt) {
	return new BigInt(times(data, anotherBigInt.data), sig == anotherBigInt.sig);
}

是不是很简单呢?其实有时 JetBrains IDE 还会给出建议,让你为你的方法加上这些注解哦。注解库也不大,就几十 kb 。如果你需要直接下载,可以点这里

 

祝你愉快。注解的实际使用还可以参照我的算法库,这是一个活生生的例子 DAZE ,请点击GitHub传送门。喜欢的话给个 star 哟(。

 

原文:第一部分 第二部分

为什么 C/C++ 初学者应该使用 CLion 而不是 Visual Studio?

初学者用 CLion 吧,折腾啥 VS 呢?
(此文略复杂,所以——
真×小白只用看加下划线的部分即可
真×小白只用看加下划线的部分即可
真×小白只用看加下划线的部分即可加粗而没下划线的不太重要)
================
=====吐槽的分割线=====
================
 
 
  • 无论是 VC 6(其实 VC 6 还是挺萌的,毕竟还能装 VAX,体验不输没装 ReSharper 的现代版本 VS)还是 VS 2015,都不真正支持 C。
  • VS 巨大,安装包大达数 G,带上 update 更大,而 CLion 安装文件只有 200 多 MB。
  • VS 的配置复杂难懂,受制于遗留太多,比如配色界面选项巨多,很多东西不知所云,而且没有预览。
  • CLion 能给出更多的分析/提示(比如 unsed variable, unreachable code, missing break 等等),对新手更有用(对人类都很有用)。
  • CLion 有更良好的开发体验(VS 的快捷键很多都是连招才能发的,比如注释、格式化)。
  • CLion 是完全跨平台的,它的 IDE 本身全平台都有版本,项目文件/构建工具(即 CMake)也是跨平台的。CMake 是微软钦定的跨平台构建工具,CoreCLR、ChakraCore 等项目都是用 CMake 构建的。
  • VS 写模板的时候 IDE 功能基本失效。参考 Visual Studio not giving me errors because of template before each function. C++
当然,空口无凭。在 tutorial 的后面我会做一个类似基准测试的东西,比较一下 CLion 和 VS 的体验,和对初学者的帮助。(非小白可以略过 tutorial)
 
 
================
=====教程的分割线=====
================
 
 
那么说了这么多,该怎么用 CLion 呢?
===========题外话===========
如果你已经装了 VS,又想享受以上好处,那就装 ReSharper C++ 吧。详情可以参考如何摆脱对Visual Studio的依赖? – Colliot 的回答
==========进入正题==========
我们知道,集成开发环境(IDE)和编译工具链(toolchain)是不必然绑定的。所以接下来我们分两步,第一步搞定 IDE,第二步搞定工具。下面开始第一步。
----------------
---第一步--安装环境----
----------------
安装 CLion 很简单,就是一个 .exe 的安装包(https://www.jetbrains.com/clion/download/),两百兆,安装好了可能一个G,安装速度也不会比一般的软件更慢(参考 VS 的 11G 起,虽然 VS 15 改进了,可是我这里网慢装不上,类似地,装 Bash for Windows 也很慢,还看运气)
---------------- --第二步--安装工具链---- ----------------
工具链方面,推荐使用 msys2(遵照这个回答安装 tdm-gcc,毫无编程基础的小白准备学习C语言,用VC6还是VS2015? – Belleve 的回答,然后使用 CLion 自带的 CMake,应该也是可行的)。msys2 是一个 cygwin 的 fork,加上了用 pacman 支持的包管理系统(不用什么 cyg-apt, apt-cyg 了),同时支持 MinGW 的编译工具链。源里一般有三份 binary(mingw 32位/64位,msys2)。
 
安装的话,只要在它的主页下载安装包就好了(MSYS2 installer),推荐 64 位(x86_64,不是i686,以下步骤可能与此相关。32位也是可以的,但是部分文字可能有所区别)的。这很小,因为只有最基本的 shell,没有预装工具链。安装之后,可以在开始菜单找到它:
三个入口,做了一些环境的设置,win32的进去,默认的工具链都会是 32 位 mingw 的版本(mingw_w64_i686),win64的进去就是64位的(mingw_w64_x86_64),msys2 进去就是 msys2 的(类似 cygwin,编译出来需要一个 dll 才能运行)。随便哪个打开都可以。
 
现在 msys2 里啥都没有,只是一个空壳,我们需要装相应的工具链。根据makefile – CMAKE_MAKE_PROGRAM not found我们需要装的东西如下:

pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-extra-cmake-modules
pacman -S mingw-w64-x86_64-make
pacman -S mingw-w64-x86_64-gdb
pacman -S mingw-w64-x86_64-toolchain

只要原样输入这四条,每次回车,并根据提示一路回车就可以了(里面似乎有重复的,实际上应该只要第一行和最后一行即可),32 位的可能需要将 w64_x86_64 改为 w32_i686。
 
现在我们有了完整的工具链,包括 gcc, make 和 cmake。这样,CLion 就能运作了。
 
初次打开 CLion,它可能会让你选择工具链,或者,以后可以在 File -> Settings (Ctrl+Alt+S)里更改:
用户上传的图片
 

如果是 32 位的,就应该在 msys2 安装目录下的 mingw32 文件夹下的相应位置,而不是 mingw64。一个能正常运行的配置应当如上图所示。

这样我们就完成了整个环境的配置,可以开始写了。

================
=====开始用的分割线====
================
打开 CLion,新建一个项目(project),你会看到两个文件,一个是你期待的 main.cpp,另一个是 CMakeLists.txt,这是 CMake 的构建清单,相当于项目文件。如果你只是要单纯地写 C++ 一个小程序,直接在 main.cpp 里写就可以了,无须做任何设置。如果你需要写 C 语言,直接写也可以,但是最好将后缀名改为 .c——方法是右键 main.cpp,选择 Refactor->Rename,或者在文件列表中选中 main.cpp,然后按 Shift+F6。

项目和构建系统方面,Clion 采用的是(两者都基于)跨平台的 CMake 元构建系统,不像VS使用的是私有项目系统和 MSBuild(?)。CMake 可以轻松支持多个目标(可执行文件、库)的构建(当然 VS 也可以,你只要添加“项目”,而不是新建“项目”或者添加”文件“。微软也支持 CMake,它开源的 .NET Core, CharkraCore 等项目都使用 CMake。

CMake 和 CLion 基于 CMake 的项目分析都能正确区分 C 和 C++ 的扩展名,给予正确的编译、构建和分析。因此,只要自己注意扩展名是 .c 还是 .cpp/.cc/.C 等即可。
实际上,构建多个目标,是小白很常见的一种需求。比方说如果在做题,总不可能为每个题目都新建一个项目吧。这时候我们就需要改动项目了。和 VS 的图形设置不同,CLion 的项目基于文本,也就是 CMakeLists.txt。

如果你要添加一个可执行文件(比方说,就是开始写另一道题目),最简单的命令就是
add_executable(可执行文件名 源码1 源码2 源码3 …),只要会这一条,应该就可以解决所有小白时代的需求了。比如 add_executable(another_solution solution2.c) 就声明了一个名为 another_solution 的可执行文件(扩展名会自动根据平台添加,比如 Linux 就没有,Windows 下就是 .exe),它是从 solution2.c 编译过来的。

CLion最新的项目模板中,CMakeLists.txt的样子应该是这样的(也就是你看见的模样):
#cmake_minimum_required(VERSION <specify CMake version here>)
project(testflight)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
set(SOURCE_FILES main.cpp)
add_executable(testflight ${SOURCE_FILES})

如果,你要像上面一样添加了名为 another_solution 的目标的话,新的 CMakeLists.txt 就应该是这样的:
#cmake_minimum_required(VERSION )
project(testflight_curses)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
set(SOURCE_FILES main.cpp) add_executable(testflight ${SOURCE_FILES})
add_executable(another_solution solution2.c)

当然,记得新建 solution2.c 这个文件,你可以通过右键任意文件->New->C++ Source File的图形界面来新建,也可以直接New->File,然后输入 solution2.c,后者相比前者,不会有模板内容,是个空文件。不过对于源文件(相比于头文件),这并没有任何影响。

 
我想,小白看到这里,大概就已经完全能满足需求了。剩下的,只要多留意 CLion 给你的提示即可。
 
(以下对小白不重要)第一行被注释了(也许没有),本来是一个限定最低 CMake 版本的指令。底下是声明项目名称。set(CMAKE_CXX_FLAGS “${CMAKE_CXX_FLAGS} -std=c++11”) 是更改编译器标识,比如默认是附加了按照C++11标准编译的标识,你可以安全地改成c++14,或者想复古,改成c++98等。注意到这是C++编译器的标识。如果是纯C,也可以指定C_FLAGS
set(CMAKE_C_FLAGS “${CMAKE_C_FLAGS} -std=c11”)等等。理论上所有flags都可以在这里指定,但是某些有更优雅的方法。一个例子是这个回答 CLion 链接库?如 lpthread 怎么设置? – 软件开发
 
它没有直接add_executable,而是定义了一个变量SOURCE_FILES,然后用这个变量去add_executable。所有存在的 CMake 变量可以在 CMake 工具窗口的 Cache 一栏查看:
用户上传的图片
 
当你新建一项(类/源文件/头文件)的时候,它会提示你是否要添加到某个target中。当然你也可以
不选,然后手动添加,如果你会初级的CMake(并不难)。
用户上传的图片
对于新建的头文件,它会自动给你建好 guard,具体内容也可以在设置里改。
 
用户上传的图片
调试什么的,就是给gdb做了个前端,中规中矩。然后在代码旁边打出当前变量的值,这也是JetBrains系统的标配。
用户上传的图片
 
整个体验就是JetBrains那一套很友好的东西,比如所有 action/setting 都是可搜索的,随时快捷键查找文件、符号等。特别是双击 Shift,什么都能找
 
用户上传的图片
唯一的问题就是你需要一些性能。可能是因为做了复杂的分析,CLion(相比 IntelliJ IDEA 等)更加吃内存和 CPU,默认的 JVM 堆大小是 2G,而索引的时候常常会吃满 CPU(见下图):
用户上传的图片
 
 
================
====测评的分割线======
================
好,说好的测评时间。开篇的对比,为了避免空口无凭,我们做一些具体项的测试:
因为这边网不好,VS 15 Preview 3 以后的在线安装包安装不了,VS 这边就选用有离线安装包的 VS 15 Preview 2(是 VS 15,不是 VS 2015 = VS 14,以下请注意区分)。CLion 版本是 2016.2。都是全新安装。
为了观感统一,我把两者的配色都调成尽可能一样的了。(我喜欢给每一类 token 一种色系,比如关键字红色,函数绿色系,变量黄色系,类型蓝色系,实际上是基于 Monokai 细化的,这样一看就知道代码的结构了。)
 
CLion 的配色界面:
用户上传的图片
VS 的配色界面:
用户上传的图片
注意 VS 无法设置斜体,也没有修饰效果(比如下划线,删除线等)。VS 的配色界面是没有整体预览的。
 
下面进行解析力测试(以下皆左 CLion,右 VS):
用户上传的图片
整体看起来差不多,也说明了 VS 15 还是比较现代的。细节处的不同隐喻了 VS 的一些坑,后面再细说。总而言之,两者的高亮区分主要有这些不一样的地方:
  • CLion 区分了 TypedefEnumClass,而VS没有
  • VS 区分了模板类普通类,CLion 没有
  • VS 区分了成员函数静态成员函数全局函数,CLion 没有
  • VS 区分了字段静态字段,CLion 没有
  • CLion 区分了重载运算符,VS 没有(也许有)
格式化方面,其实 VS 没什么问题了现在。我记得当年 VS 2013 的体验是很差的,但是 VS 15 足够称得上现代了(VS 2015 不知道,装着 ReSharper C++,不想测)。
用户上传的图片
两者都能正确格式化,在输入代码的时候也会正确自动格式化(VC 6 就会有蜜汁缩进)。
 
其实 VS 15 还是比较标准的。这应该是微软转型的一步棋,一如用友好的方式开源 .NET Core。(虽然 Preview 2 版本还是默认预编译头)。VS 15 默认模板是 main ,而不是 _tmain,看起来也不强制你使用 scanf_s。这个似乎应该归功于 CRT 的进一步标准化?我记得 VS 2015 还需要 #define _CRT_SECURE_NO_WARNINGS 来着,VS 15 已经不需要了
 
我们注意到以上两图中,CLion 的那边经常会出现一些标为灰色的、或者带有波浪下划线的内容,而 VS 却没有任何提示。这是 CLion 多给的提示。具体而言,CLion 会提示
  • 未使用的变量、函数
  • 可能未初始化的局部变量
  • 循环中未更新循环条件
  • 无限循环
  • 不可达代码
  • 一些可以简化的地方,比如某些 if -> 三目表达式
  • 拼写检查(支持驼峰、下划线等命名风格)
  • 字符串中不正确的 escape
  • 不正确的 printf/scanf 族函数的格式字符串
  • ……
以上 VS 都无动于衷,可以在以下专门的对比图,以及几乎所有的对比图中验证,或者自行验证。
 
特别这种 scanf 族的格式错误(忘记取地址),是初学者经常容易陷入的。
 
用户上传的图片
再来一图:
 
用户上传的图片
VS 只提示了不正确的 else。当然,CLion 也没有觉得没有 body 的 if 有问题,这是个遗憾。
未使用的包含文件,在 CLion 里也会被标灰,而 VS 一如既往地无动于衷。(随便在网上找了一段 OI 代码,注意到那个宏也是 unused)。
用户上传的图片
不过,这个应该跟 Alt+Enter 自动导入符号是一套机制,因此就会有同样的坑。比如在 gcc 的标准库 libstdc++ 里,std::string 实际上是在 stringfwd.h 里边儿,所以它可能提示你 include 这个文件,而不是标准的 string。同样地,由于 libstdc++ 的 iostream 间接包含了 stringfwd.h,所以你只包含 iostream(甚至是 iosfwd?)就能用 std::string 了,而 string 这个(为了跨编译器)应当包含的头文件却可能被标灰。这可能导致困惑,所以见仁见智了。
用户上传的图片
 
接下来是模板和面向对象:
 
用户上传的图片
注意到 CLion 的右侧行号旁边,标注出了类和方法的继承/覆写关系,而 VS 没有。注意到 Derived 模板中,VS 对于 value 的标注是灰色的。Derived 的构造函数调用的 Base<Derived<T>> 的构造函数少了一对括号,VS 无动于衷,而 <<< 多打了一个 <,VS 也没有发现。以上所有问题,CLion 都正确地给出了标注。
 
用户上传的图片
当然类似的问题对于非模板代码,VS 还是能发现的。
 
用户上传的图片
VS 15 似乎不支持 auto 参数,因此给了报错。CLion (打开了 std=c++14)支持,但是不能在悬浮的时候推导出变量类型。对于 auto lambda,两者都能推到出返回值类型,但是加入了模板元编程后,CLion 歇菜了,而 VS 居然能正确推导出返回值类型(f2 是 long, f3 是 float)
 
用户上传的图片
甚至对于值模板、乃至于对值特化的值模板都照推不误(funcRet4 是 int, funcRet5 是 const char *),令人叹为观止,而 CLion 早已败下阵来。可见 VS 还是有两把刷子的,这也是 VS 在本次比较中唯一更好的地方
还有重构之类的,就不比了。VS 从 2015 开始应该提升了不少(可以参考All about C++ Refactoring in Visual Studio 2015 Preview),但是再提升也不会比 Clion(==ReSharper C++) 更好,就没必要比了。
这些都不是什么紧要的问题,没什么技术含量,相信也不难做。无非是个体验问题罢了。可是整个 IDE 不就是关于体验问题么
 
其他的还有一些功能,比如之前提到的“Alt+Enter 自动导入符号”,这都是 JetBrains 系列的标配。以及众多的插件和对 Python 的支持(其实就是 PyCharm)都是很好的(为什么 C++ IDE 都喜欢支持 Python?)
 
不过对初学者来说,这些都是次要的。主要还是以上提到的一些内容。写了一篇微小的教程,谢谢大家。
========================================
说了这么多,对初学者有多少帮助也说不准。存在这样的观点,初学者就应该使用 plain 的文本编辑器,比如notepad.exe,以期或者更多的历练。这并没有什么值得反驳的地方。另一方面,“初学者”的概念也相当含糊,是从哪里来(比如刚上大学?),又要到哪里去呢(参加算法竞赛,还是写软件?)?问题的背景不可一概而论,没有一贯的应对方案,因而也无法有过多的企图。写这篇东西,徒然在想象中期望为我想象中的初学者带来一些帮助罢了。

编程少年们追梦的地方