作为一个 TabNine 的资深免费用户,在遇到 Github Copilot 的第一晚,我就无可救药地爱上了后者,并义无反顾地卸载了前者。作为 AI coding 时代的杰出代表,Github Copilot 做到了比我自己还更懂我。以前,写代码的时候,我基本都是气沉丹田紧锁眉头沉默不语;自从用上了 Copilot,画风就变成了:艾玛,我靠,我去,真的假的,不可能吧。
药,药,切克闹。
比如下面的代码(双下划线内部代码为自动补齐代码):
#[derive(Debug)]
struct State {
// for a given user, how many rooms they're in
user_rooms: DashMap<String, DashSet<String>>,
// for __a given room, how many users are in it__
__room_users: DashMap<String, DashSet<String>>__,
tx: broadcast::Sender<Arc<Msg>>,
}
当我写完 user_rooms 的定义后,换行,打出注释 // for
,copilot 能够自动提示可以补齐后续的注释,当我 tab 使用这个补齐后,紧接着它有帮我补齐 room_users 那一行。
在我进一步实现 get_user_rooms
这个方法时,写完方法名,就得到了完整定义和实现的自动补齐提示:
#[derive(Debug, Clone, Default)]
pub struct ChatState(Arc<State>);
impl ChatState {
pub fn new() -> Self {
Self(Default::default())
}
pub fn get_user_rooms__(&self, username: &str) -> Option<&DashSet<String>> {
self.0.user_rooms.get(username)
}__
这个方法的参数是我期待的,但返回值并非我所想要的。于是我把补全的代码稍作修改。随后继续写 下一个方法 get_room_users
,有了上个方法的铺垫,这次 copilot 完全正确地补全了我的意图:
impl ChatState {
pub fn new() -> Self {
Self(Default::default())
}
pub fn get_user_rooms(&self, username: &str) -> Vec<String> {
self.0
.user_rooms
.get(username)
.map(|rooms| rooms.clone().into_iter().collect())
.unwrap_or_default()
}
pub fn get_room_users__(&self, room: &str) -> Vec<String> {
self.0
.room_users
.get(room)
.map(|users| users.clone().into_iter().collect())
.unwrap_or_default()
}__
}
当然,copilot 让人感觉惊艳的同时,也还有不小的瑕疵:代码可能并不能正确编译,有时甚至能够编译通过但有逻辑 bug。比如我定义了如下消息类型:
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MsgData {
Join,
Leave,
Message(String),
}
在对其进行 match 的过程中,copilot 猜出了我主要的意图,并提供了如下的代码补全:
match msg.data {
MsgData::Join => {
state
.user_rooms
.entry(msg.username)
.or_insert_with(DashSet::new)
.insert(msg.room);
state
.room_users
.entry(msg.room) // 这里有 ownership 问题
.or_insert_with(DashSet::new)
.insert(msg.username); // 这里有 ownership 问题
}
MsgData::Leave => {
if let Some(v) = state.user_rooms.get_mut(&msg.username) {
v.remove(&msg.room);
if v.is_empty() {
// 这里有死锁问题
state.user_rooms.remove(&msg.username);
}
}
if let Some(v) = state.room_users.get_mut(&msg.room) {
v.remove(&msg.username);
if v.is_empty() {
// 这里有死锁问题
state.room_users.remove(&msg.room);
}
}
}
...
};
其中,一开始处理 MsgData::Leave
的代码结构和 MsgData::Join
的类似:
MsgData::Join => {
state
.user_rooms
.entry(msg.username)
.or_insert_with(DashSet::new)
.remove(msg.room);
state
.room_users
.entry(msg.room)
.or_insert_with(DashSet::new)
.remove(msg.username);
msg
}
但当我将其修改成这一句
if let Some(v) = state.user_rooms.get_mut(&msg.username) {
后,copilot 给我了不一样的补全方案(如上),预判了我的预判。
如果我们看上面的完整的补全代码,可以看到,撰写出这样的代码可能要几分钟时间。这是 copilot 能够为我省下的时间。不过,代码中有几处注释中提到的错误,尤其那个死锁的问题:
if let Some(v) = state.user_rooms.get_mut(&msg.username) {
v.remove(&msg.room);
if v.is_empty() {
// 这里有死锁问题,需要先 drop(v)
state.user_rooms.remove(&msg.username);
}
}
还额外花了我几分钟时间才揪出来(我在 unit testing 时发现的,否则花费时间更长)。不过,扪心自问的话,我自己撰写相同的代码,可能也很难第一时间意识到这里需要 drop(v)
。
在过去使用 copilot 的一个来月,我感觉有几点特别重要:
虽然目前 copilot 对各种语言都有不错的支持,不过我建议在动态类型语言下,如 Python,JavaScript 要特别小心自动生成的代码,因为即便 copilot 提供了错误的代码(比如访问一个对象下并不存在的域),这些语言也不会报错,可能会把很多错误的发现推迟至运行时。
目前我使用的感受是,AI coding 可以使程序员变得更加高效,尤其是优秀的程序员。它会进一步拉开设计良好,命名良好的系统和设计并不好的系统的开发效率,也会拉开优秀程序员和普通程序员的效率。我个人感觉,虽然大家的效率都提高了,但优秀开发者的效率提升会更大。原因有二:1. 优秀开发者的思路更清晰,设计和代码风格更好,AI 可以更容易「猜测」出来作者的意图;2. 优秀开发者阅读和理解 AI 给出的代码建议会更快,会更快地决定是否采用(以及采用后修改)推荐的代码。
因为代码产出的速率提升,同样的工作原本需要十个人月,使用了 AI Coding 后,可能八个人月就能完成。所以,我觉得在某种程度上,未来 AI coding 可能会减少一些对程序员的需求。