📖 功能介紹頁 | ⬇️ 從 VS Code Marketplace 安裝
免費使用,VS Code 內按
Ctrl+P貼上ext install mark-ku.refactory也可直接安裝。
前言
把主力開發搬進 VS Code 之後,最不習慣的是重構(不改變程式行為,只把它整理得更好讀)。
在傳統商業 IDE 裡按一個鍵,這個位置能做的事會全部列出來。到了 VS Code,這些功能散在各處,而且少了一大半。就算有,產生出來的碼也不像你專案的碼:介面該放哪個資料夾?DI 註冊寫在哪?'use client' 該不該帶過去?通用工具都不知道。
所以我做了 Refactory,已上架 Marketplace,免費使用,34 個重構動作 + 38 條壞味道檢查、18 種語言。
這篇只講怎麼操作:游標放哪、按什麼鍵、按完變成什麼樣子。
一、安裝(30 秒)
VS Code 內按 Ctrl+P 貼上 ext install mark-ku.refactory,或在 Extensions 面板搜尋 Refactory。裝完就能用,不用設定。
要用 C# 功能的話電腦要有 .NET 8 runtime;沒裝只會關掉 C# 那一半,React / TypeScript 不受影響。
二、只要記一組快速鍵
| 按鍵 | 什麼時候按 |
|---|---|
Ctrl+Alt+Shift+T | 主力。把游標所在位置能做的事全部列出來 |
Ctrl+. | 原生燈泡選單,Refactory 的動作也嵌在裡面 |
Ctrl+Shift+P → 打 Refactory | 對「整個檔案」操作的指令 |
| 編輯器裡按右鍵 | 選單裡也有一項「重構這裡…」,跟第一列同一個東西 |
記住第一個就夠了。

兩個會幫到你的地方:最近用過的動作會浮到最上面;不能用的動作也會列出來並告訴你原因,不用猜。
習慣按 Ctrl+. 的人不用改習慣,動作一樣找得到:

三、React / Next.js
3-1 把一段 JSX 抽成元件

怎麼操作
- 選取一段完整的 JSX 區塊
- 按
Ctrl+Alt+Shift+T- 選
Extract JSX into a component(同檔)或…into a new component file(新檔)- 打名字,Enter
選取這段 → 它產生這樣(props 型別自動推導,不用自己填):
1-<section className="hero">
2- <h1>{active}</h1>
3- <button onClick={() => setActive('next')}>Next</button>
4-</section> 1+interface NewComponentProps {
2+ active: string
3+ setActive: (value: string) => void
4+}
5+
6+function NewComponent({ active, setActive }: NewComponentProps) {
7+ return (
8+ <section className="hero">
9+ <h1>{active}</h1>
10+ <button onClick={() => setActive('next')}>Next</button>
11+ </section>
12+ )
13+}原本的位置自動換成 <NewComponent active={active} setActive={setActive} />。

注意狀態列的 4 selections:呼叫點、interface 名稱、函式名稱、型別引用同時進入編輯狀態,打一次字四處一起改。
抽到新檔案時會順手做這些:'use client' 帶過去、只搬用到的 import、共用常數改成 export + import(不是複製一份)、檔案位置與命名照你專案的慣例。
跨檔案操作會先跳 Refactor Preview 面板,逐條確認後才寫入。
3-2 把一坨 state 邏輯抽成自訂 hook
怎麼操作:選取連續的幾行(至少含一個 hook 呼叫)→
Ctrl+Alt+Shift+T→Extract into a custom hook→ 打名字
1-const [query, setQuery] = useState('')
2-const [results, setResults] = useState<Post[]>([])
3-useEffect(() => {
4- if (!query) return
5- fetchPosts(query).then(setResults)
6-}, [query]) 1+function useSearch() {
2+ const [query, setQuery] = useState('')
3+ const [results, setResults] = useState<Post[]>([])
4+ useEffect(() => {
5+ if (!query) return
6+ fetchPosts(query).then(setResults)
7+ }, [query])
8+
9+ return { query, setQuery, results }
10+}useEffect 的依賴陣列絕對不會被動到。搬完覺得依賴怪怪的,代表這段本來就有問題,那是另一個 commit 的事。
3-3 把 JSX 包起來:條件、.map()、fragment
三個都是選取一段完整 JSX 之後按 Ctrl+Alt+Shift+T,差別只在選單挑哪一項。
包成條件顯示:
1 <div>
2- <b>{n}</b>
3 </div>1 <div>
2+ {condition && (
3+ <b>{n}</b>
4+ )}
5 </div>包成 .map():
1 <ul>
2- <li>{name}</li>
3 </ul>1 <ul>
2+ {items.map((item) => (
3+ <li key={item}>{name}</li>
4+ ))}
5 </ul>condition、items、item 都是就地可編輯的游標點,Tab 跳下一個,打字直接改名,不用回頭找。
key={item} 是先幫你補上去、同時附一條警告:沒有人知道你的資料拿什麼當唯一鍵,所以它給一個編得過的預設值並明說「請換成穩定 id」,而不是安靜地留一個空位,讓 React 事後才在 console 抱怨。
選兩個以上的兄弟節點時,選單改成提供 fragment(<>…</>);只選一個根節點時才會出現 .map()——包單一節點成 fragment 沒有意義,而 .map() 一次包兩個節點會產生無效的 JSX。
3-4 const X: FC<Props> 轉成函式宣告
怎麼操作:游標放在元件宣告那一行 →
Ctrl+Alt+Shift+T→ 選轉成函式宣告
1 interface Props {
2 title: string
3 }
4
5-const Card: FC<Props> = ({ title }) => {
6 return <b>{title}</b>
7-}
8-
9-export default Card1 interface Props {
2 title: string
3 }
4
5+export default function Card({ title }: Props) {
6 return <b>{title}</b>
7+} 兩個細節:型別從變數搬到參數上;下面那行單獨的 export default Card 被吸收掉,不會留一行孤兒。簡寫的 body(=> <b>{title}</b>)會補成 return。
這個動作一定會附一條警告:FC 隱含一個 children prop,轉成函式宣告之後就沒有了。如果你的元件其實有人塞 children 進來,型別會從這一刻開始才真的檢查你。泛型元件(<T,>)則直接拒絕,因為 FC<Props> 本來就寫不出泛型。
3-5 改一個 prop 的名字,連所有呼叫點一起改
這是純手工最容易漏的一種:改了型別、改了解構,漏掉別的檔案裡的 <Card title="…" />。
怎麼操作:游標放在 props 型別裡的那個屬性上 →
Ctrl+Alt+Shift+T→ 選改 prop 名稱 → 打新名字
1 interface CardProps {
2- title: string
3 count: number
4 }
5
6-export function Card({ title, count }: CardProps) {
7- return <b>{title}: {count}</b>
8 }1 interface CardProps {
2+ heading: string
3 count: number
4 }
5
6+export function Card({ heading, count }: CardProps) {
7+ return <b>{heading}: {count}</b>
8 }同一次動作也會把別的檔案裡的呼叫點改掉:
1 export function Panel() {
2- return <Card title="hello" count={1} />
3 }1 export function Panel() {
2+ return <Card heading="hello" count={1} />
3 }旁邊的 count 一個字都沒動。解構時本來就寫了別名({ title: heading })的話,它只改型別成員,不會去動你已經取好的區域名稱。
遇到 <Card {...props} /> 這種展開的呼叫點會直接拒絕:那個物件裡到底有沒有 title,要靠型別推導才知道,一個 lex 層級的索引不該假裝自己知道。
3-6 Inline variable 與 import 路徑轉換
Inline variable(把只用一次的中間變數就地展開):
1 export function Panel() {
2- const label = 'hello'
3- return <b>{label}</b>
4 }1 export function Panel() {
2+ return <b>{'hello'}</b>
3 }用了兩次以上、而且初始化沒有副作用時會全部展開,並在需要的地方補上括號保住運算優先序(n * 2 展開成 {n * 2}{n * 2})。三種情況它會拒絕:let(值會變)、初始化有副作用又被用了不只一次(會多跑幾次),還有被 useEffect 依賴陣列點名的變數(展開等於偷偷改了依賴)。
import 路徑轉換(游標放在路徑字串上):
1-import { posts } from '../../data/posts'
2-import type { TPost } from '../../data/types'1+import { posts } from '@/data/posts'
2+import type { TPost } from '@/data/types'alias 是從管這個檔案的那份 tsconfig.json 的 paths 讀出來的,不用另外設定;import type、export … from、動態 import()、純副作用 import 都認得,原本用單引號或雙引號也照舊。兩個方向可以來回轉,轉回去會拿到一模一樣的原文。
3-7 游標放哪:完整對照表
游標放錯位置是這類工具最常卡的地方,直接查這張表:
| 想做的事 | 游標 / 選取位置 |
|---|---|
| 抽 JSX 成元件 | 選取一段完整的 JSX 區塊 |
| 抽自訂 hook | 選取連續的幾行,至少含一個 hook 呼叫 |
const X: FC<Props> 轉函式宣告 | 放在元件宣告上 |
包 fragment / 條件 / .map() | 選取一段完整的 JSX 區塊 |
| Inline variable(變數就地展開) | 放在 const 宣告或它的任一引用上 |
| 單一 import 轉 alias / 相對路徑 | 放在路徑字串上(如 '../../data/posts') |
| 整檔 import 一次轉換 | Ctrl+Shift+P → Refactory: TypeScript: Convert Imports… |
| 把宣告搬到獨立檔案 | 放在被宣告的那個名稱上(引用處會一起改) |
| Safe delete(安全刪除) | 放在被宣告的名稱上;有人在用會拒絕並列出誰在用 |
| 改 prop 名稱(含所有呼叫點) | 放在 props 型別裡的那個屬性上 |
'use client' 修正 | 不用操作,會出現在 Problems 面板,Ctrl+. 套用 |
三個提醒:JavaScript 也適用(產出不帶型別註記,副檔名跟著來源走);import alias 不用設定,直接從管這個檔的 tsconfig.json 讀;整檔轉換刻意只放命令面板,避免游標一動就跳出來。
3-8 'use client' 自動提醒(Next.js)
不用操作,在 Next.js 專案裡打字就會出現:
| 情況 | 它怎麼處理 |
|---|---|
用了 useState 卻忘了加 directive | 出現警告,Ctrl+. → Add 'use client' directive |
| 加了,但同檔又 export 只能跑在伺服器的東西 | 不給快速修復,並直說:要把 client 的部分拆成獨立元件,不是把邊界往上搬 |
| 加了但根本用不到 | 最低等級的提示,拿掉就不會被打包進瀏覽器 |
3-9 React 壞味道即時提醒
「壞味道」=編得過、但之後會讓你痛的寫法。打字時就畫波浪線,每條都附 Ctrl+. 快速修復。

- 元件裡面又定義元件(每次 render 都變新型別,state 直接歸零,最嚴重的一條)
- 直接改動 state:對
useState的值做items.push(...),畫面永遠不會更新 useEffect:沒有依賴陣列、callback 寫成 async、訂閱沒有 cleanup、effect 太肥- 用
.map()的 index 當key - hook 太多、props 太多、元件太大、JSX 巢狀太深、JSX 裡塞巢狀三元運算
- Next.js 專案用原生
<img>(該用next/image) - 一般 TypeScript:明寫
any、空的catch、console.log、檔案過長
嚴重度刻意分級:會炸的是 warning、結構問題是 information、品味問題是最淡的 hint,真正重要的訊號才不會被淹掉。
3-10 一個 15 行的元件,五條壞味道
下面這個檔案是外掛自己的測試素材,也是第五節儀表板截圖裡那個 LiveTicker.tsx。它編得過、跑得起來、看起來人畜無害,五條 warning 全中:
1 import { useEffect, useState } from 'react';
2
3 export function LiveTicker({ symbols }: { symbols: string[] }) {
4- const [quotes, setQuotes] = useState<string[]>([]);
5
6 useEffect(() => {
7- setInterval(() => {
8- quotes.push(symbols[0]);
9 }, 1000);
10- });
11-
12- const Row = ({ text }: { text: string }) => <li>{text}</li>;
13
14- return <ul>{quotes.map((q) => <Row text={q} />)}</ul>;
15 }1 import { useEffect, useState } from 'react';
2
3+let nextId = 0;
4+
5+const Row = ({ text }: { text: string }) => <li>{text}</li>;
6+
7 export function LiveTicker({ symbols }: { symbols: string[] }) {
8+ const [quotes, setQuotes] = useState<{ id: number; text: string }[]>([]);
9
10 useEffect(() => {
11+ const timer = setInterval(() => {
12+ setQuotes((prev) => [...prev, { id: nextId++, text: symbols[0] }]);
13 }, 1000);
14+ return () => clearInterval(timer);
15+ }, [symbols]);
16
17+ return (
18+ <ul>
19+ {quotes.map((q) => <Row key={q.id} text={q.text} />)}
20+ </ul>
21+ );
22 }五條分別是:
| 行 | 規則 id | 它為什麼會咬你 |
|---|---|---|
useEffect(() => {…}) | react.effectWithoutDeps | 沒有依賴陣列,每次 render 都重跑一次 |
setInterval(…) | react.effectMissingCleanup | 沒有 cleanup,重新掛載後舊的那顆還在跑(StrictMode 下直接兩顆) |
quotes.push(…) | react.mutatedState | 改的是同一個陣列,reference 沒變,React 看不到要重繪什麼 |
const Row = … | react.componentInComponent | 每次 render 都是一個新的元件型別,React 會卸載它、丟掉它的 state 與 DOM |
.map((q) => <Row …>) | react.missingKey | 沒給 key,React 安靜地退回用 index |
這五條都是 warning,因為每一條都會在 runtime 咬人,而不是「風格不夠好」。修法就是右邊那一欄:定時器拿變數接住並在 cleanup 清掉、push 換成產生新陣列的 setQuotes、Row 搬到模組層、key 綁在資料本身的穩定 id 上。
四、C#

| 動作 | 它做什麼 |
|---|---|
| 加注入依賴 | 一次改好六個地方(下面詳講) |
| Extract Interface | 抽出介面,放到你專案實際擺介面的資料夾 |
| 介面成員同步 | 介面加了新成員,一鍵在所有實作補上 |
| DI 註冊 | 把 service 註冊進 DI container |
ConfigureAwait 補齊 | 整檔補上 ConfigureAwait(false) |
| 產生 XML 文件 | 幫成員產生 XML 註解(介面有寫就沿用它的措辭) |
| 控制器 clone 到下一版 | v1 controller 複製成 v2,該改的引用一起改 |
這七個只在按 Ctrl+Alt+Shift+T 時出現,不會主動跳燈泡(判斷游標在不在 class 裡要問 Roslyn,每次移動都問太慢)。
4-1 加注入依賴:一個動作改六個地方
依賴注入(DI)就是「不要自己 new,改成叫別人送進來」。手動加一個依賴要改六個地方,漏一個就編不過。

怎麼操作:游標放在 class 內任何位置 →
Ctrl+Alt+Shift+T→Add injected dependency…→ 輸入型別(例如IPlanService)→ 輸入命名空間(可留空)
// 原本的 class
public class CouponService : ICouponService
{
/// <summary>優惠券用戶端。</summary>
private readonly ICouponClient couponClient;
/// <param name="couponClient">優惠券用戶端。</param>
public CouponService(ICouponClient couponClient)
{
this.couponClient = couponClient;
}
}
按下去之後,這六個地方同時改好:
using敘述(依字母序插入,不是接在最後面)- 新的
private readonly欄位 - 欄位的 XML 註解
- 建構子參數
<param>標籤(插在跟參數位置對應處,StyleCop 會檢查順序)- 建構子裡的
this.賦值
六筆編輯、一次 undo 全部復原,落地前先讓你看過:

面板上是五列不是六列——欄位和它上面那行 XML 註解是同一段插入,所以併在同一列顯示。由上到下對應的就是 using、欄位(含註解)、<param>、建構子參數、this. 賦值。
產出的風格是從你正在編輯的那個 class「量」出來的,不是讀設定檔。欄位有沒有底線前綴、賦值有沒有 this.、有沒有 XML 註解,照現有的樣子生。
兩種情況它會直接拒絕:這個 class 有手動 new Foo(...) 的呼叫點(硬塞 null! 編得過但 runtime 會炸);還有「DI 註冊了沒」它從不假裝檢查過,會明說沒驗證並強制走預覽。
4-2 Extract Interface:目標資料夾跟鄰居學
怎麼操作:游標放在 class 名稱上 →
Ctrl+Alt+Shift+T→Extract Interface
內建版本會把新檔寫在 class 旁邊,但很多分層專案的介面固定住在鏡像資料夾:
BLL/
├── Contracts/Services/Coupons/ICouponService.cs ← 介面在這
└── Services/Coupons/CouponService.cs ← 實作在這
Refactory 的做法是:看同資料夾其他已經有介面的 class,查它們的介面住在哪,取多數決。推導結果會寫在提示裡(例如「依 3 個兄弟類別推得」),學不到也直說。產出的介面檔會帶上你專案的 copyright header、XML 註解與 BOM。
4-3 整檔補上 ConfigureAwait(false)
函式庫程式碼漏掉 ConfigureAwait(false),在舊版同步環境裡是經典的 deadlock 來源。這個動作一次掃完整個檔案,只補漏掉的那幾個。
怎麼操作:游標放在 class 內任何位置 →
Ctrl+Alt+Shift+T→Add missing ConfigureAwait(false) in this file
1 public async Task RunAsync()
2 {
3 var a = await this.client.GetAsync().ConfigureAwait(false);
4- var b = await this.client.PostAsync();
5- await this.client.FlushAsync();
6 }1 public async Task RunAsync()
2 {
3 var a = await this.client.GetAsync().ConfigureAwait(false);
4+ var b = await this.client.PostAsync().ConfigureAwait(false);
5+ await this.client.FlushAsync().ConfigureAwait(false);
6 }第一行本來就有,所以不會被加第二次。await Task.Yield() 會被跳過(它根本沒有 ConfigureAwait)。全部補完之後再按一次,它會直接說沒事可做,而不是產生一份空的編輯。
4-4 幫成員產生 XML 文件
怎麼操作:游標放在方法 / 屬性 / 建構子的宣告上 →
Ctrl+Alt+Shift+T→Document this member
1 public Task<int> GetAsync(string code, int page)
2 {
3 return Task.FromResult(0);
4 }1+/// <summary></summary>
2+/// <param name="code"></param>
3+/// <param name="page"></param>
4+/// <returns></returns>
5 public Task<int> GetAsync(string code, int page)
6 {
7 return Task.FromResult(0);
8 }參數有幾個就有幾個 <param>,順序跟著簽章走;void 不會生 <returns>;建構子直接套 .refactory.json 裡設定的那句話(預設是 Initializes a new instance of the <see cref="Service"/> class.)。
這個成員實作了某個介面、而介面那邊已經寫過註解的話,它會把介面的措辭抄過來——連中文都照抄——並附一條提醒:StyleCop 的 SA1625 討厭一字不差的重複文件,你可能想改寫一下。已經有註解的成員它會拒絕,不會覆蓋你寫過的東西。
4-5 C# 壞味道檢查(開檔與存檔時跑)
每條一樣附 Ctrl+. 快速修復:
- Sync-over-async:在 Task 上用
.Result/.Wait()/.GetAwaiter().GetResult(),經典 deadlock 來源 - async 程式裡出現
Thread.Sleep、事件處理器以外的async void - 建構子注入太多(超過 5 個)、空的
catch、catch 了沒 rethrow - 方法太長、class 太大、檔案太長、巢狀太深
- magic number、巢狀三元運算、public 可變欄位
- 一個檔案塞好幾個頂層型別、TODO / FIXME 標記
門檻是在真實專案上校準過的(85 檔的 Next.js 後台、1,312 檔的分層 C# solution)。OnModelCreating、ConfigureServices 這種本來就長的方法已豁免長方法規則。
五、Code Health 儀表板
怎麼操作:
Ctrl+Shift+P→ Refactory: Code Health Dashboard,或點狀態列右下角的$(pulse)計數器

剛打開時看到的是「目前這個檔案」(上圖就是還沒掃描的狀態:四張數字卡、沒有 Copy report)。按右上角 Scan workspace 才會擴到整個工作區,也才會多出平均健康分數、Trends 與 Hotspots——下面 5-3 那張圖就是掃完之後的樣子。
裡面有三個名詞,白話講一次。
5-1 圈複雜度:這個函式有幾條路可以走
數字越大,你要看懂它、或要寫測試蓋滿它,要顧的分支越多。算法是從 1 開始,每遇到一個岔路就 +1(if、迴圈、每個 case、catch、三元運算子、每個 && || ??):
function getDiscount(user, cart) { // 起始 1
if (!user) return 0 // +1 → 2
if (user.isVip && cart.total > 1000) { // +1 (if) +1 (&&) → 4
return 0.2
}
for (const item of cart.items) { // +1 → 5
if (item.onSale) return 0.1 // +1 → 6
}
return user.coupon ? 0.05 : 0 // +1 → 7
}
這個函式是 7,預設門檻 15,所以很健康。
補充:匿名 callback 和 lambda 算進最近的具名函式,所以 React 元件的複雜度會包含裡面的 inline handler 和 .map() body(讀這個元件時本來就要一起讀懂)。
5-2 健康分數:100 分起跳往下扣
| 扣分項目 | 扣多少 |
|---|---|
| 每個 warning(會炸的問題) | 10 分 |
| 每個 suggestion(結構問題) | 3 分 |
| 每個 hint(品味問題) | 1 分 |
| 函式複雜度超過門檻的部分 | 超出多少扣多少 |
假設某檔案有 2 warning、3 suggestion、1 hint,加一個複雜度 20 的函式(門檻 15,超出 5):
扣分 = 2×10 + 3×3 + 1×1 + 5 = 35
分數 = 100 − 35 = 65 → D
等第:90 以上 A、80–89 B、70–79 C、60–69 D、60 以下 F。
分數不會擋 build 也不會跳警告,它只用來排序,回答「我今天該先看哪個檔案」。
5-3 Hotspots:常改 × 不健康 = 最該先修
一個很爛但半年沒人動的檔案,跟一個天天被改的檔案,優先順序完全不同。所以把兩件事相乘:
熱點分數 = 最近常不常改 × 有多不健康(100 − 健康分數)
關鍵是相乘不是相加,任一邊是 0 結果就是 0:很爛但沒人動的、天天改但很乾淨的,都不會排前面;又常改又不健康的才會排到最上面。
「常不常改」是跑一次 git log,數這個檔案最近 90 天被幾個 commit 動過。沒有 git 的話這區塊直接不出現,不會跳錯誤煩你。

5-4 面板怎麼用
掃過一次之後,由上到下是:五張數字卡(smells / warnings / suggestions / hints,加上掃描後才出現的平均健康分數)→ Trends(每掃一次記一根長條,最近 30 次,看得出這週比上週好還是壞)→ Hotspots → Most complex functions(最複雜的 10 個,點了就跳過去)→ 檔案清單(等第徽章 + 逐條問題,點一下跳到那一行)。
| 按鈕 | 做什麼 |
|---|---|
| Scan workspace | 掃整個工作區。直接讀硬碟,不開檔也不灌爆 Problems 面板,可中途取消。TS/JS 與 C# 各自最多 2,000 檔 |
| Copy report | 把發現轉成 Markdown 複製,直接貼進 PR 描述(掃過一次才會出現) |
| Refresh | 重新整理 |
| 右上角那排色點 | 八種面板主題,第一個是跟著 VS Code 佈景走 |
5-5 一鍵清掉機械性的問題
怎麼操作:
Ctrl+Shift+P→ Refactory: Clean Up This File
- TypeScript / JavaScript:刪掉所有「獨佔一行」的
console.log(...) - C#:把 catch 裡的
throw ex;改成throw;(保住 stack trace)
跑完會回報做了什麼(例如 3 console.logs removed, 1 rethrow fixed),一個 undo 全部復原。
六、一鍵交給本機 Claude Code 修
本機有裝 Claude Code CLI 且在 PATH 上的話,會多出三個入口:
入口一:任一條壞味道的
Ctrl+.選單多一項Fix with Claude Code…入口二:儀表板每個檔案右側的✦ Review(Hotspots 區塊還有✦ Review top 3) 入口三:Ctrl+Shift+P→ Refactory: Deep Review This File with Claude (AI),不用先開儀表板
入口一會開整合終端機,跑你自己的 claude session,prompt 帶上規則 id、檔案、行號與訊息,並要求改動最小化。
入口二與入口三會先寫一份 briefing 給 Claude(行數、函式數、最高與平均複雜度、健康分數與等第、最複雜的幾個函式、最近哪幾個 commit 動過它、所有找到的問題),請它產出排好優先順序的重構建議。briefing 寫在 .refactory/reviews/,那個資料夾自己帶一個內容為 * 的 .gitignore,不會被 commit。
你的登入、你的模型、你的核可流程,外掛只負責把 prompt 寫好。沒有任何東西會被自動套用,你看著 diff 落地。
七、另外 13 種語言:最常用的那幾個重構
Python、Go、Java、Kotlin、Rust、PHP、C、C++、Objective-C、Objective-C++、Dart、Swift、Scala 各有 11 個結構性重構,不用 language server、不用 SDK、什麼都不用裝。開一個 .go 檔,就算你連 Go 擴充都沒裝,這些照樣出得來。
| 動作 | 在哪些語言 |
|---|---|
| Invert if condition | 全部 |
| Merge nested if · Split if condition | 全部 |
| Add braces · Remove braces | 有大括號的那幾種 |
| Introduce variable · Inline variable | 全部(TS / JS 除外,編輯器內建的更好用) |
| Apply De Morgan's law | 全部 |
Replace if with ?: | 除了 Go(它沒有三元運算子) |
| 轉插值字串 | Python、C#、Kotlin、PHP、Dart、Swift |
.format() / % 轉 f-string | Python |
怎麼操作:游標放在
if關鍵字或它的條件式上(不是 body 裡隨便哪裡)→Ctrl+Alt+Shift+T
游標一定要在條件式上,否則四層巢狀的 if 會同時全部提供,你分不出哪個是哪個。
注意:除了 Introduce variable 和轉插值字串(這兩個讓給 VS Code 內建的型別感知版本),上面這些在 TypeScript / JavaScript 一樣用得到。下面的例子挑不同語言寫,只是為了展示同一個動作在各語言會產生該語言的寫法。
7-1 Invert if:把 happy path 拉回左邊
游標放在
if上 →Invert if condition
1-if err == nil {
2- process(data)
3-} else {
4 return err
5 }1+if err != nil {
2 return err
3+} else {
4+ process(data)
5 }它做的是條件取反 + 兩個分支對調,不會自作主張幫你把 else 拆掉——那是另一個決定,不該混在同一個動作裡。條件本來就有 ! 的話會把它拿掉,而不是疊成 !!;!(a && b) 這種會連括號一起收乾淨變成 a && b。
一個別人不會提的小地方:if (a < b) 反轉成 if (a >= b),在浮點數上這兩者不等價(遇到 NaN 兩邊都是 false)。運算元看起來像浮點數時它會跳提醒並強制走預覽,市面上的工具多半直接翻給你。
7-2 Merge nested if:把金字塔壓平
游標放在外層的
if上 →Merge nested if
1 function f(user) {
2- if (user) {
3- if (user.isActive) {
4- send(user)
5- log(user)
6- }
7 }
8 }1 function f(user) {
2+ if (user && user.isActive) {
3+ send(user)
4+ log(user)
5 }
6 }Python 也吃,而且會連縮排一起收回來(用的是 and,不是 &&):
1 def f(user):
2- if user:
3- if user.is_active:
4- send(user)
5- log(user)1 def f(user):
2+ if user and user.is_active:
3+ send(user)
4+ log(user) 反過來的 Split if condition 是同一個選單裡的另一項:把 if (a && b) 拆回兩層,通常是因為你要在中間那層插一個 else。
7-3 De Morgan:!(a && b) 兩個方向都能走
1-if (!(a && b)) {
2 x()
3 }1+if (!a || !b) {
2 x()
3 }兩個方向都提供,if (!a || !b) 上按下去會收回成 if (!(a && b)),來回轉會拿到一模一樣的原文。比較運算子不會被硬加否定,而是直接翻面:
1-if (!(a === 1 && b > 2)) {
2 x()
3 }1+if (a !== 1 || b <= 2) {
2 x()
3 }Python 用的是它自己的字:if not (ready and done): ⟷ if not ready or not done:。已經是 if (a && b) 這種沒有否定的條件不會提供這個動作——沒有東西可以分配。
7-4 if 轉條件運算式:每種語言寫法不一樣
同一個動作,產出的是那個語言真正的寫法,不是硬套 ?::
1-if (user.isAdmin()) {
2- role = "admin";
3-} else {
4- role = "guest";
5-}1+role = user.isAdmin() ? "admin" : "guest"; 1 def role(user):
2- if user.is_admin:
3- return "admin"
4- else:
5- return "guest"1 def role(user):
2+ return "admin" if user.is_admin else "guest" Kotlin 拿到的是 if-expression(role = if (user.isAdmin) "admin" else "guest"),Rust 是大括號版的 if-expression(role = if user.is_admin { "admin" } else { "guest" };)。兩個分支必須是同一件事——都是 return,或都是賦值給同一個東西——否則不提供。
7-5 Python:.format() / % 轉 f-string
游標放在字串字面值裡 →
Convert to an f-string
1-msg = "Hello, {}!".format(name)
2-row = "{:>8}|{:<8}".format(left, right)
3-title = "Hi {who}".format(who=user.name)1+msg = f"Hello, {name}!"
2+row = f"{left:>8}|{right:<8}"
3+title = f"Hi {user.name}"對齊格式(:>8)與轉換旗標(!r)原封不動搬過去,{1} … {0} 這種明寫位置的也會照著對回正確的引數。
"%d" % value 刻意不轉:"%d" % 3.7 的結果是 "3",f"{3.7}" 是 "3.7"——這不是格式差異,是值變了。只有 %s 和 %r 轉過去意思完全不變,所以只有這兩個會轉。
八、設定
| 設定 | 預設 | 說明 |
|---|---|---|
refactory.preview | multiFile | 什麼時候走預覽面板。帶警告的計畫一律強制預覽 |
refactory.dotnetPath | "" | 指定 C# 引擎用的 dotnet 路徑 |
refactory.disabledLanguages | [] | 要完全避開的語言 id |
refactory.smells.react / .csharp | 開 | 各自關掉一半的壞味道檢查 |
refactory.developerMode | false | 開啟 Dev: 診斷指令 |
refactory.keymap | none | 設 riderStyle 會多綁 Ctrl+Alt+M / Ctrl+Alt+V 到 VS Code 內建的抽取動作。預設關掉,因為非美式鍵盤上 Ctrl+Alt+<字母> 跟 AltGr+<字母> 分不出來 |
教它你的專案慣例:.refactory.json
慣例是專案的屬性,不是個人偏好,所以放 repo 根目錄、進版控、全團隊共用。所有欄位都可省略,沒寫的就用自動偵測的:
{
"version": 1,
"typescript": {
"components": { "declaration": "exportDefaultFunction", "propsStyle": "interfaceSuffixProps" },
"hooks": {
// 先寫特定路徑、最後放 catch-all,第一個符合的贏
"location": [
{ "when": "src/components/admin/**", "dir": "src/components/admin/_hooks" },
{ "when": "**", "dir": "src/hooks" }
],
"fileNaming": "camelCase"
}
},
"smells": { /* 個別規則關閉或調門檻,id 見 docs/SMELLS.md */ },
"exclude": ["**/node_modules/**", "**/.next/**"]
}
exclude 同時也管壞味道檢查,所以打開 generated 的 migration 檔不會滿屏波浪線。
九、按了卻「什麼都沒出現」
怎麼操作:設定
"refactory.developerMode": true→Ctrl+Shift+P→ Refactory: Dev: Why Is Nothing Offered Here?
它會直接告訴你:哪份 tsconfig 管這個檔、推導出哪些 alias、語言認不認得、檔案能不能正常解析、游標是不是在字串或註解裡、哪些動作是刻意讓給編輯器內建的。
最常見的元兇是前面有一個沒閉合的字串,會讓後面整個檔案失效。還是想不通,Refactory 這個 output channel 有完整 trace。
注意事項
- 繁體中文介面:VS Code 語言設繁中就會看到「重構這裡…」「程式碼健康儀表板」
- C# 需要 .NET 8 runtime;純前端的工作階段根本不會啟動 .NET sidecar
- Kotlin 要先裝任一個 Kotlin 外掛(VS Code 自己沒註冊
kotlin語言 id) - Ruby 刻意不支援:詞法結構沒有 parser 就是真的有歧義,一張百分之一機率出錯的分析表比不支援更糟
- TypeScript / JavaScript 上刻意不提供 Introduce variable 和轉插值字串,內建版本懂型別,比較好用
- 不會拖慢 VS Code:不載入 solution、不載入 TypeScript program,跨檔問題用宣告索引回答(上千檔一秒內建完)
結論
通用的重構各家原廠都有做。但知道你的專案把介面放哪、DI 註冊寫在哪、'use client' 該不該加,這件事只有懂你 repo 的工具做得到。
- 安裝:VS Code Marketplace 搜 Refactory,或
ext install mark-ku.refactory - 工具頁:Refactory 介紹頁(功能總覽、截圖與介紹影片)
裝起來按一下 Ctrl+Alt+Shift+T,你就知道少的那一味是什麼了。



























留言