只需要寫寫注釋,就能生成能夠運行的代碼?對于程序員群體來說,這絕對是一個提高生產(chǎn)力的超級工具,令人難以置信。實際上,早在2021年6月,微軟和OpenAI聯(lián)手推出了GitHub Copilot這一AI編程工具。它能夠根據(jù)開發(fā)者的輸入和上下文,生成高質(zhì)量的代碼片段和建議。這個工具看上去很好用很神奇,但我相信很多人仍然懷有一定的懷疑態(tài)度。讓我們來親身體驗一下,看看效果如何。
首先我們需要訪問 https://github.com/features/copilot/ Copilot的官方網(wǎng)站開通copilot的使用權(quán)限。
我們需要先登陸github的賬戶,然后點擊開始免費試用后會跳轉(zhuǎn)到開通確認(rèn)頁面,這里會提示我們開通后擁有60天的免費體驗的時間。
雖然有免費體驗的時間,但是我們還是要選擇一個支付方案,因為試用期間不會真正扣費,所以我們大膽選擇按月交費,每月10美元。點擊獲取訪問Copilot功能按鈕后就會進入下一步支付詳情的頁面,在這里我們要填上姓名、地址和國家。
填寫完成就可以進入支付方式表單頁面,我們按照頁面要求正確填寫好信用卡信息。
填寫完成后,我們點擊保存支付詳情按鈕,當(dāng)網(wǎng)站的后臺服務(wù)完成信用卡可用性驗證后,頁面會跳轉(zhuǎn)到Copilot配置代碼提示匹配范圍的頁面,這里我們需要選擇是否匹配公共代碼,如果選擇Allow,Copilot就會在整個Github的公共代碼庫中進行匹配然后給出代碼建議,否則的話它只會在當(dāng)前Github賬戶的organizations中進行匹配。因為我們希望體驗Copilot的神奇,所以這里我們選擇Allow
當(dāng)然,這個選項是可以更改的,我們可以到 https://github.com/settings/copilot 配置頁面中進行更改。選擇完成后,點擊保存并開始使用
,就會彈出支付成功的頁面,頁面上也提示我們可以去支持Copilot的IDE中安裝插件了。
接下來我們在IDE中安裝Copilot插件,這里我們使用的是VSCode,其它支持的IDE的安裝方式大家可以在 https://docs.github.com/zh/copilot/configuring-github-copilot/configuring-github-copilot-in-your-environment 頁面中找到,同時一些快捷鍵和使用技巧在官方文檔中也都清晰的描述。
首先我們打開 VSCode 的擴展商店,搜索Copilot,會看到有許多相關(guān)的擴展,我們選擇Github的官方擴展,注意不要選錯了,然后點擊安裝。
安裝完成后,右下角會出現(xiàn)處于加載狀態(tài)的Copilot小圖標(biāo),同時會提示我們需要先登錄Github賬號,才可以使用Copilot
按照指引完成登錄以后,Copilot的小圖標(biāo)就能正常顯示了。
接下來,就讓我們體驗一下Copilot的神奇之處,驗證一下它生成的代碼質(zhì)量如何。這里要注意一點,Copilot是使用編程語言中的注釋作為交互方式的,比如對于Javascript來說,輸入//
就可以激活Copilot,然后它會根據(jù)我們代碼的上下文以及我們已經(jīng)輸入的部分注釋內(nèi)容,在代碼庫中匹配出合適的內(nèi)容推薦出來,如果我們認(rèn)為推薦的合適,可以按 tab
鍵應(yīng)用推薦內(nèi)容。然后按下回車開始進行下一行內(nèi)容的提示。
斐波那契數(shù)列
作為一個經(jīng)典的數(shù)學(xué)問題,Javascript實現(xiàn)起來也是有多種方式的,當(dāng)我們注釋中寫完Fabonacci 數(shù)列求和函數(shù),函數(shù)名是 fabonacci
以后,一路按tab和回車,Copilot給我們生成了這樣一份代碼。
// Fabonacci 數(shù)列求和函數(shù),函數(shù)名是 fabonacci // 例如 fabonacci(10) // 55 function fabonacci(n) { if (n <= 1) { return n; } return fabonacci(n - 1) + fabonacci(n - 2); } console.log(fabonacci(10));
運行結(jié)果如下
當(dāng)我寫完第一行注釋的描述后,它自動就推薦生成了第二行對于函數(shù)的進一步描述信息,而最終Copilot生成的代碼也是按照這個注釋中的描述實現(xiàn)的,可以看到結(jié)果完全沒問題,代碼的格式以及寫法都算是很規(guī)范的。不過這個方法采用的是普通的遞歸方式實現(xiàn),在數(shù)據(jù)量大的時候效率會降低,因此現(xiàn)在我們想用尾遞歸的方式實現(xiàn)這個方法,看看Copilot能不能幫我們寫出正確的代碼。
我們把注釋信息修改為Fabonacci 數(shù)列求和函數(shù),用 尾遞歸 方式實現(xiàn),函數(shù)名是 fabonacci
,還是一路tab加回車,基本沒有手動輸入什么代碼,最后生成的代碼如下:
// Fabonacci 數(shù)列求和函數(shù),用 尾遞歸 方式實現(xiàn),函數(shù)名是 fabonacci // 用法:fabonacci(10) // 55 function fabonacci(n, a = 1, b = 1) { if (n <= 1) { return b; } return fabonacci(n - 1, b, a + b); } console.log(fabonacci(10));
運行結(jié)果如下
看到結(jié)果,我們會發(fā)現(xiàn),第二次果然是用尾遞歸實現(xiàn)了一個Fabonacci求和方法,但是兩次運行的結(jié)果不一樣,而且第二次,運行的結(jié)果和注釋中的要求也不太一樣,難道是代碼有錯誤?經(jīng)過我們對代碼的審查,發(fā)現(xiàn)這個尾遞歸代碼中,參數(shù)變量a=1
表示數(shù)列從1開始,而剛才的普通遞歸形式的代碼中沒有對數(shù)列的開始值進行處理,默認(rèn)是從0開始的,所以兩次同樣是計算長度為10的數(shù)列合,結(jié)果卻產(chǎn)生了分歧。我們將注釋細(xì)化一下,要求數(shù)列的第一項為0 看看結(jié)果如何:
// Fabonacci 數(shù)列求和函數(shù),用 尾遞歸 方式實現(xiàn),函數(shù)名是 fabonacci // 用法:fabonacci(10) // 55 // 數(shù)列要求從 0 開始 function fabonacci(n, a = 0, b = 1) { if (n <= 1) { return b; } return fabonacci(n - 1, b, a + b); } console.log(fabonacci(10));
運行結(jié)果如下
這回沒問題了,兩次運行的結(jié)果都是一致的了。
由此可見,如果我們在注釋中沒有非常明確的描述出必要的約束調(diào)教時,Capilot生成的代碼可能會和我們的預(yù)期稍有不同。
貪吃蛇
剛剛我們使用Copilot解決了一個數(shù)學(xué)問題。這表明對于有標(biāo)準(zhǔn)算法思路的代碼,它可以很好地推薦和補全代碼?,F(xiàn)在,我們要讓Copilot幫我們制作一個小游戲,這種代碼更偏向業(yè)務(wù)層面,需要更多的對代碼上下文的理解。讓我們看看這次Copilot的表現(xiàn)如何。
貪吃蛇這個小游戲相信大家都玩兒過,就是在一個n*m的區(qū)域中,我們使用鍵盤控制小蛇上下左右移動,吃到食物后小蛇長度加一;蛇頭碰到自身或窗口邊緣后,游戲失敗。我們首先定義了一個Map類,用來構(gòu)造游戲區(qū)域以及是否觸及邊界,繪制單元格等方法。
/** * ------------------------------------------------------------------ * Map 類:用來構(gòu)造游戲的區(qū)域范圍,包括區(qū)域的行列等。 * ------------------------------------------------------------------ */ class Map { /** * 構(gòu)造函數(shù),初始化地圖的行列等參數(shù) */ constructor(col, row) { this.col = col || 24; this.row = row || 24; this.matrix = []; this.init(); }; /** * init() 方法:初始化地圖 */ init() { if (this.row < 1 || this.col < 1) { throw new Error('Row and Column sizes must be at least 1'); } this.matrix = []; for (let i = 0; i < this.row; i++) { this.matrix[i] = []; for (let j = 0; j < this.col; j++) { this.matrix[i][j] = 0; } } }; /** * initMatrix() 方法:初始化地圖的矩陣table,并添加到element元素上 * @param:element 要添加到的元素 */ initMatrix(element) { // 基于 this.col和this.row 創(chuàng)建一個table, // 生成的table中每個cell存儲到this.map的對應(yīng)位置 // 然后把table添加到element元素上 let table = document.createElement("table"); let tbody = document.createElement("tbody"); for (let i = 0; i < this.col; i++) { let tr = document.createElement("tr"); for (let j = 0; j < this.row; j++) { let td = document.createElement("td"); this.matrix[i][j] = tr.appendChild(td); } tbody.appendChild(tr); } table.appendChild(tbody); element.appendChild(table); }; /** * randomPoint(x, y) 方法:初始化地圖的矩陣table,并添加到element元素上 * @param:x 點的x坐標(biāo) * @param:y 點的y坐標(biāo) * @return:返回一個隨機點 * ------------------------------------------------------------------ */ randomPoint(x,y){ let point = []; point[0] = Math.floor(Math.random()*x)+ 1; point[1] = Math.floor(Math.random()*y)+ 1; return point ; }; /** * generateFood() 方法:根據(jù)地圖的行列生成一個隨機的食物 * @return:食物的坐標(biāo) * ------------------------------------------------------------------ */ generateFood() { const food = this.randomPoint(this.col, this.row); return food; }; /** * drawCell(x, y, className): 根據(jù)坐標(biāo)和類名,繪制一個cell * @param:x cell的x坐標(biāo) * @param:y cell的y坐標(biāo) * @param:className 要添加的類名 */ drawCell(x, y, className) { this.matrix[x][y].className = className; }; /** * hitWall(point) 方法:判斷是否撞墻 * @param:point 點的坐標(biāo) * @return:true or false */ hitWall(point) { if(point instanceof Array){ if(point[0]<0||point[0]>this.col-1||point[1]<0||point[1]>this.row-1){ return true; } } return false; }; }
以及一個Snake類,用于描述小蛇的數(shù)據(jù)結(jié)構(gòu),判斷是否觸及小蛇身體等。
/** * ------------------------------------------------------------------ * Snake 類:用來構(gòu)造貪吃蛇,包括蛇的身體等。 * ------------------------------------------------------------------ */ class Snake { /** * 構(gòu)造函數(shù),初始化蛇的身體數(shù)組等參數(shù) */ constructor() { this.body = []; this.init(); }; /** * init() 方法:初始化蛇的長度位置 */ init() { // 初始化蛇的位置 this.body.push([1,3]); this.body.push([1,2]); this.body.push([1,1]); }; /** * onSnake(point, includeHead = false) 方法:判斷是否在蛇身上 * @param:point 點的坐標(biāo) * @param:includeHead 是否考慮作為腦袋的第一個cell * @return:true or false */ onSnake(point, includeHead = false) { // 判斷是否在蛇身上 if (point instanceof Array) { for (var i = includeHead ? 0 : 1; i < this.body.length; i++) { if (point[0] == this.body[i][0] && point[1] == this.body[i][1]) { return true; } } } return false; } }
上面兩部分是我們準(zhǔn)備的基礎(chǔ)類,接下來就是Copilot大展身手的時候了,我們要實現(xiàn)一個Game類,用來實現(xiàn)游戲的初始化,小蛇的移動,游戲結(jié)果的判定等功能。按照Copilot的用法,我們先寫注釋,因為時業(yè)務(wù)邏輯,所以我們就盡量多的用注釋將代碼需要的業(yè)務(wù)邏輯描述清楚,看看結(jié)果如何。經(jīng)過一系列的輸入,tab和回車,Copilot幫我們生成了如下的代碼:
/** * ------------------------------------------------------------------ * Game 類:這款游戲的主類,包括了游戲的初始化,蛇的移動等。 * ------------------------------------------------------------------ */ class Game { /** * 構(gòu)造函數(shù),初始化游戲,包括地圖,蛇,食物等 */ constructor() { // 蛇的實例 this.snake = new Snake(); // 地圖的實例 this.map = new Map(); // 食物的坐標(biāo) this.food = []; // 游戲的定時器 this.timer = null; // 游戲是否停止 this.stop = true; // 用于存儲當(dāng)前按下的按鍵 this.pressedKey = 0; // 用于存儲當(dāng)前蛇的移動方向 this.nextX = 0; this.nextY = 1; this.init(); }; /** * init() 方法:初始化游戲類 */ init() { // 初始化游戲 // 生成地圖 this.map.initMatrix(document.body) // 將蛇繪制在屏幕上 this.drawSnake(); // 生成食物 this.newFood(); // 綁定bind方法,用來監(jiān)聽鍵盤的按下事件 window.addEventListener("keydown", this.bind.bind(this)); }; /** * newFood():在地圖上生成一個新的食物。 * @returns */ newFood() { this.food = this.map.generateFood(); // 調(diào)用snake實例的onSnake方法判斷食物是否在蛇身上 if (this.snake.onSnake(this.food, true)) { this.newFood(); return false; } // 調(diào)用map實例的drawCell方法將食物繪制在屏幕上,類名為food this.map.drawCell(this.food[0],this.food[1], "food"); }; /** * drawSnake(): 調(diào)用 map 實例的drawCell接口將貪吃蛇繪制在屏幕上 */ drawSnake() { // 遍歷snake實例的body數(shù)組,獲取到每個cell的坐標(biāo),分別存儲為_tempX和_tempY for(let i=0;i < this.snake.body.length; i++ ){ let _tempX = this.snake.body[i][0], _tempY = this.snake.body[i][1]; // 調(diào)用map實例的drawCell方法將_tempX和_tempY繪制在屏幕上,類名為snake this.map.drawCell(_tempX, _tempY, "snake"); } }; /** * bind(_e) 方法:用來監(jiān)聽鍵盤的按下事件 * @param _e 事件對象 * @returns true or false */ bind(_e) { // 將作為參數(shù)的事件對象賦值給e let e = _e; // 獲取按下的鍵盤的鍵碼 let keycode = e ? e.keyCode : 0; // 判斷按下的鍵碼,如果是空格鍵,則暫停或繼續(xù)游戲,如果是上下左右鍵,將keyCode賦值給pressedKey switch (keycode) { case 32: // 如果 this.stop 被設(shè)置成 true 了,就繼續(xù)游戲,否則清除定時器,暫停游戲 if (this.stop) { this.timer = setInterval(this.move, 200); } else { clearInterval(this.timer); } this.stop = !this.stop; break; // 上下左右鍵 case 37: case 38: case 39: case 40: // 如果游戲不是暫停狀態(tài),將keyCode賦值給pressedKey if (!this.stop) { this.pressedKey = keycode; } break; } return false; }; /** * start() 方法:游戲的主要邏輯 */ start() { // 游戲的主要邏輯 // 首先定義蛇頭所在的位置 let x = this.snake.body[0][0]; let y = this.snake.body[0][1]; // 定義蛇尾所在的位置 let tail = this.snake.body[this.snake.body.length - 1]; // 定義游戲是否結(jié)束的標(biāo)志 let over = false; // 根據(jù)按下的按鍵來改變蛇的移動方向 switch (this.pressedKey) { // 向左 case 37: this.nextX = 0; this.nextY = -1; break; // 向上 case 38: this.nextX = -1; this.nextY = 0; break; // 向右 case 39: this.nextX = 0; this.nextY = 1; break; // 向下 case 40: this.nextX = 1; this.nextY = 0; break; } // 根據(jù)蛇的移動方向來改變蛇頭的位置 x += this.nextX; y += this.nextY; // 判斷是否吃到食物 if (x === this.food[0] && y === this.food[1]) { // 如果吃到食物,將食物的位置添加到蛇的body數(shù)組中第一項作為新的蛇頭 this.snake.body.unshift([x, y]); // 生成新的食物 this.newFood(); } else { // 如果沒有吃到食物,判斷蛇是否撞到了自己或者撞到了墻 // 先調(diào)用 map 實例的 hitWall 方法判斷蛇頭是否撞到了墻 if (this.map.hitWall(x, y)) { over = true; } else if (this.snake.onSnake([x, y], false)) { // 再調(diào)用 snake 實例的 onSnake 方法判斷蛇頭是否撞到了自己 over = true; } // 如果撞到墻或者自己了,設(shè)置游戲結(jié)束的標(biāo)志為 true if (over) { // 清除定時器 clearInterval(this.timer); // 彈出游戲結(jié)束的提示 alert("游戲結(jié)束"); return false; } } }; /** * move() 貪吃蛇移動 */ move() { // 蛇移動 // 使用定時器調(diào)用start方法,每隔200毫秒調(diào)用一次 this.timer = setInterval(this.start.bind(this), 200); }; } // 實例化一個Game對象 new Game();
我們將文件保存為snake.js,然后創(chuàng)建一個html文件用來作為這個小游戲的頁面文件,代碼如下:
<!DOCTYPE> <html> <head> <title>貪吃蛇</title> <head> <style> * { margin: 0; padding: 0; border: 0px; } body { background-color: #37393b; } table { border-collapse: collapse; overflow: hidden; border: 1px solid #d0e3f2; margin: 8px auto; } td { display: table-cell; width: 16px; height: 16px; background: #87a4a9; border: 1px #bccfdf solid; } #info { width: 450px; margin: 24px auto; text-align: center; font-size: 14px; color: #fff;} .snake { background: #42e5d4; } .empty { background: #87a4a9; } .food { background: #f1ec5a; } </style> </head> </head> <body> <div> <p id="info">空格鍵控制開始,方向鍵控制小蛇的移動方向</p> </div> <script type="text/javascript" src="snake.js"></script> </body> </html>
在瀏覽器中加載后,我們看到游戲界面可以正常加載出來,那關(guān)鍵能不能運行呢? 我們按下空格鍵,卻返現(xiàn)小蛇并沒有移動,打開瀏覽器的控制臺,看到有如下報錯:
Copilot在實現(xiàn)綁定上下文指針this
的代碼時,使用了某代碼片段中的bind方法,但是這個方法并不是Javascript解釋器的方法,而我們也沒有定義這個方法,所以出錯了。知道原因了,我們就修復(fù)一下,使用Javascript標(biāo)準(zhǔn)的綁定上下文方法修改一下,而且有兩處地方使用了bind,所以我們修改如下:
class Game{ /** * init() 方法:初始化游戲類 */ init() { // 初始化游戲 // 生成地圖 this.map.initMatrix(document.body) // 將蛇繪制在屏幕上 this.drawSnake(); // 生成食物 this.newFood(); // 綁定bindKeydown方法,用來監(jiān)聽鍵盤的按下事件 let _this = this; document.onkeydown = _this.bindContext(_this.bindKeydown, _this); }; ... /** * bindContext(fn,context) 方法:用來繼承運行上下文 * @param fn 需要繼承的方法 * @param context * @returns 繼承以后的方法 */ bindContext(fn,context) { return function(){ return fn.apply(context,arguments); } }; ... /** * move() 貪吃蛇移動 */ move() { // 蛇移動 // 使用定時器調(diào)用start方法,每隔200毫秒調(diào)用一次 // 使用本類中的 bindContext 方法繼承 this let _this = this; // 先清除定時器 if (_this.timer) { clearInterval(_this.timer); } // 再重新設(shè)置定時器 _this.timer = setInterval(_this.bindContext(_this.start, _this), 200); }; }
改好以后運行,發(fā)現(xiàn)還是有問題,會提示 move 方法中的_this.bindContext
不存在:
這就比較奇怪了,因為已經(jīng)在方法中綁定了運行時的this上下文,說明有調(diào)用move的地方?jīng)]有繼承運行上下文指針this,去代碼中找了一下,果然有一處異步調(diào)用沒有傳遞上下文指針,因為這里其實不用延時調(diào)用,我們把這里改成直接調(diào)用就可以:
/** * bindKeydown(_e) 方法:用來監(jiān)聽鍵盤的按下事件 * @param _e 事件對象 * @returns true or false */ bindKeydown(_e) { // 將作為參數(shù)的事件對象賦值給e let e = _e; // 獲取按下的鍵盤的鍵碼 let keycode = e ? e.keyCode : 0; // 判斷按下的鍵碼,如果是空格鍵,則暫?;蚶^續(xù)游戲,如果是上下左右鍵,將keyCode賦值給pressedKey switch (keycode) { case 32: // 如果 this.stop 被設(shè)置成 true 了,就繼續(xù)游戲,否則清除定時器,暫停游戲 if (this.stop) { this.move(); } else { clearInterval(this.timer); } this.stop = !this.stop; break; // 上下左右鍵 case 37: case 38: case 39: case 40: // 如果游戲不是暫停狀態(tài),將keyCode賦值給pressedKey if (!this.stop) { this.pressedKey = keycode; } break; } return false; };
改好以后運行,這回我們的小蛇可以成功動起來,并且可以吃到食物啦,不過當(dāng)我們完了一下以后,發(fā)現(xiàn)小蛇在撞墻的時候并沒有提示結(jié)束游戲,反而報錯了,報錯如下:
經(jīng)過檢查,我們發(fā)現(xiàn)時調(diào)用是否碰撞到墻壁的方法時參數(shù)傳遞的問題,并沒有按照方法的參數(shù)要求將坐標(biāo)作為一個參數(shù)以數(shù)組的形式傳遞到方法中,于是我們進行了修改:
... } else { // 如果沒有吃到食物,判斷蛇是否撞到了自己或者撞到了墻 // 先調(diào)用 map 實例的 hitWall 方法判斷蛇頭是否撞到了墻 if (this.map.hitWall([x, y])) { over = true; } else if (this.snake.onSnake([x, y], false)) { // 再調(diào)用 snake 實例的 onSnake 方法判斷蛇頭是否撞到了自己 over = true; } // 如果撞到墻或者自己了,設(shè)置游戲結(jié)束的標(biāo)志為 true if (over) { ...
這回,我們的小蛇可以正常吃到食物,撞墻也能正確提示游戲結(jié)束了,但是還是發(fā)現(xiàn)一個問題,當(dāng)我們控制方向鍵的時候,如果按下和小蛇行進方向的反方向按鍵的時候,也提示游戲結(jié)束,這是不對的,我們需要修改一下,當(dāng)按下和小蛇行進方向相反的按鍵時,對于下一個點的坐標(biāo)不做任何改變,修改如下:
... // 定義游戲是否結(jié)束的標(biāo)志 let over = false; // 根據(jù)按下的按鍵來改變蛇的移動方向 switch (this.pressedKey) { // 向左 case 37: if (this.nextY != 1) { this.nextX = 0; this.nextY = -1; } break; // 向上 case 38: if (this.nextX != 1) { this.nextX = -1; this.nextY = 0; } break; // 向右 case 39: if (this.nextY != -1) { this.nextX = 0; this.nextY = 1; } break; // 向下 case 40: if (this.nextX != -1) { this.nextX = 1; this.nextY = 0; } break; } ...
這樣,我們的小游戲就可以比較完美的運行了。文章來源:http://www.zghlxwxcb.cn/news/detail-490628.html
通過制作這個小游戲,我們可以看出Copilot在程序開發(fā)方面非常友好。它不僅能夠生成正確的代碼,還能理解開發(fā)者的意圖和上下文,生成符合實際需求的代碼片段。但是在某些特殊場景下,我們?nèi)匀恍枰獙ι傻拇a進行調(diào)整,特別是在編寫涉及敏感信息和安全問題的代碼時,我們需要更加謹(jǐn)慎,避免泄露機密信息。此外,我們還應(yīng)該遵守版權(quán)和知識產(chǎn)權(quán)相關(guān)規(guī)定,以確保不侵犯他人的權(quán)利。文章來源地址http://www.zghlxwxcb.cn/news/detail-490628.html
到了這里,關(guān)于微軟和OpenAI聯(lián)手推出了GitHub Copilot這一AI編程工具,可根據(jù)開發(fā)者的輸入和上下文,生成高質(zhì)量的代碼片段和建議的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!