iOS開發——OC編碼規範

weixin_34185560發表於2017-06-20

建議

  • 為了專案的“整潔性”不建議同時使用多語言開發;
  • 在匯入第三方框架之前,充分考慮匯入此框架的必要性和風險,遵循能不用第三方就不用的原則;
  • 使用你能想到的最簡單、清晰的、合理的方式解決問題,如果你寫的程式碼複雜難懂,請不用思考了就是設計有問題,請換種方式。

一、標頭檔案的匯入(#import)

  • 寫法模板

#import "當前類頭髮檔案"

#import <系統庫>

#import <第三方庫>

#import "其他類"

儘量按照先系統類 第三方類 自己寫的類順序匯入 中間不能有空格

  • 建議的寫法
#import "YJHomeViewController.h"        // 當前類標頭檔案

// controllers 
#import "YJLoginViewController.h" 

// views
#import "YJCycleScrollView.h" 

// models 
#import "YJMessageModel.h"

// others
#import "UINavigationBar+Awesome.h" 
  • 不建議的寫法
#import "YJHomeViewController.h"        // 當前類標頭檔案
 
#import "YJLoginViewController.h" 
#import "YJCycleScrollView.h" 
#import "YJMessageModel.h"
#import "UINavigationBar+Awesome.h" 

二、@Class的寫法

在.h檔案中儘量使用@class,引用標頭檔案

寫法模板
@class class1, class2;

  • 建議的寫法
@class UIView, UIImage;
  • 不建議的寫法
@class UIPress;
@class UIPressesEvent;

三、@Interface的寫法

寫法模板

@interface 類名 : 父類 <協議1, 協議2>

@interface和類名中間一個空格

類名後緊跟“ : ”之後空格加上父類協議之間用,空格分割

  • 建議的寫法
@interface AppDelegate : UIResponder <UIApplicationDelegate, UITableViewDataSource>
  • 不建議的寫法
@interface AppDelegate:UIResponder<UIApplicationDelegate,UITableViewDataSource>

四、@property關鍵詞的使用

物件 strong

基本變數 assign

Xib控制元件、代理用 weak

字串、block使用 copy

對於一些弱引用物件使用 weak

對於需要賦值記憶體物件 copy

五、@property的寫法

@property(關鍵詞, 關鍵詞) 類 *變數名稱; // 註釋

關鍵詞用,空格分割 類前後空格

  • 建議的寫法
@property (nonatomic, copy) NSString  *productID;         // 產品標識
@property (nonatomic, copy) NSString  *status;            // 狀態
  • 不建議的寫法
@property (nonatomic, copy) NSString  * productID;         // 產品標識
@property (nonatomic, copy) NSString  * status;            // 狀態

六、控制元件儘量放到匿名分類中且分組

  1. 控制元件組

  2. 資料來源組<建議帶上模型型別>

  3. 互動變數組

  • 建議的寫法
@interface YJHomeViewController ()  
@property (nonatomic, strong) UIView *navigationBarView;        // 導航欄  
@property (nonatomic, strong) YJCycleScrollView *advertiseView; // 廣告欄 
@property (nonatomic, strong) YJPopularRecommendView *popularView; // 熱門推薦檢視

@property (nonatomic, strong) NSMutableArray <YJBannerModel *> *adsArray;          // 廣告 
@property (nonatomic, strong) NSMutableArray <YJProductModel *> *popularInfoArray; // 熱門推薦 

@property (nonatomic, assign) NSInteger nowPage;            // 當前頁碼 
@property (nonatomic, assign) NSInteger selectedIndex;

@end 

七、類的功能模組建議按以下方式分組

  1. view的生命週期方法

  2. 初始化方法

  3. 代理

  4. 事件處理

  5. 網路請求

  6. 懶載入方法

  • 建議的寫法
#pragma mark - view的生命週期方法

#pragma mark - setup

#pragma mark - <DataSource>

#pragma mark - <Delegate>

#pragma mark - actions

#pragma mark - request dataSource

#pragma mark - setter and getter

八、const的使用

  1. 開頭用k標識

  2. k + 專案名字首 + 作用名稱 + 模組名(可不寫)

  • 建議的寫法
static NSString *const kYJOpenAccountAgreement = @"yjlc://openAccount";     // 開戶 
static NSString *const kYJAddRechargeAgreement = @"yjlc://addRecharge";     // 充值 
static NSString *const kYJAddWithdrawAgreement = @"yjlc://addWithdraw";     // 提現

定義外部可使用的const常量

  • 宣告
UIKIT_EXTERN NSString *const kNoticationUpdateCartList;
  • 實現
NSString *const kNoticationUpdateCartList = @"kNoticationUpdateCartList";

對於只在.m內部宣告的const,建議新增static

  • 建議寫法
static NSString *kInvestmentCellID = @"InvestmentCellID";

九、enum的定義

  1. 對應的enum寫到對應的類中,方便尋找和使用

  2. 使用NS _ ENUM和NS _ OPTIONS進行定義

  • 建議的寫法
typedef NS_ENUM(NSInteger, UIViewAnimationCurve) {
    UIViewAnimationCurveEaseInOut,         // slow at beginning and end
    UIViewAnimationCurveEaseIn,            // slow at beginning
    UIViewAnimationCurveEaseOut,           // slow at end
    UIViewAnimationCurveLinear,
};

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
  • 不建議的寫法
typedef enum {
    GBAppRunDeveloperModeDebug,
    GBAppRunDeveloperModePreRelease,
    GBAppRunDeveloperModeRelease
}GBAppRunDeveloperMode;

十、三元操作符

簡單的if else判斷,儘量使用三目運算子,提高程式碼的簡潔性和可讀性

  • 建議寫法
NSInter result = a > b ? a : b;
  • 不建議寫法
NSInter result;
if (a > b) {
    result = a;
} else {
    result = b;
}

十一、單例

建議使用 dispatch _ once 來建立單例

  • 建議寫法
+ (instancetype)sharedInstance {
    static id sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
   }); 
    return sharedInstance;
}

十二、init方法

返回型別應該使用instancetype而不是id

  • 建議寫法
- (instancetype)init { 
    if (self = [super init]) {
        // ...
    }
    return self;
}

十三、寫模組化的程式碼

好的模組化程式碼需要滿足以下幾點:

  1. 避免寫太長的函式(函式長度最好都不超過40行,因為我的眼球不轉的話,最大的視角只看得到40行程式碼);

  2. 製造小的工具函式,讓邏輯看起來更清晰,提高程式碼複用率;

  3. 每個函式只做一件邏輯相關的事情,比如讀檔案函式就只做讀取檔案操作,寫入檔案函式就只做寫入功能,絕對不摻合其他邏輯;

  4. 避免使用全域性變數和類成員(class member)來傳遞資訊,儘量使用區域性變數和引數(減少對全域性變數和類成員的維護成本和出錯率);

十四、寫可讀的程式碼

有些人以為寫很多註釋就可以讓程式碼更加可讀,然而卻發現事與願違。註釋不但沒能讓程式碼變得可讀,反而由於大量的註釋充斥在程式碼中間,讓程式變得障眼難讀。而且程式碼的邏輯一旦修改,就會有很多的註釋變得過時,需要更新。修改註釋是相當大的負擔,所以大量的註釋,反而成為了妨礙改進程式碼的絆腳石。

所以如何寫可讀的程式碼,我們首先要明白什麼時候寫註釋,什麼時候不用寫註釋。

什麼時候寫註釋:

  1. 有時,你也許會為了繞過其他一些程式碼的設計問題,採用一些違反直覺的作法。這時候你可以使用很短註釋,說明為什麼要寫成那奇怪的樣子。(儘量沒有)

  2. 後臺返回的引數,有時確實需要寫簡短的註釋來標明這個欄位的意思。

  3. 網路請求的方法,我們有事需要標註方法的作用、引數和返回值。

  4. 當一塊功能模組,需要嚴格按照某個業務邏輯執行才行時,可以使用適當的註釋。

  5. 合理的使用TODO:,當你未完成此功能又急需去完成其它功能的時候,請放一個TODO:在這並解析清楚未完成的工作,保證自己不會忘掉,並在完成後刪掉(這個很重要)。

  • 建議寫法
// 屬性

@property (nonatomic, copy) NSString *userName;     // 使用者暱稱
@property (nonatomic, copy) NSString *address;      // 使用者地址
// 網路請求

/**
 *  獲取訂單詳情
 *  
 *  @param orderId  訂單編號
 *  
 *  @return 網路請求
 */
+ (NSURLSessionDataTask *)getOrderInfoById:(NSString *)orderId completionHandler:(void(^)(OrderModel *order, NSError *error))completionHandler;
// 有較強的業務邏輯及TODO:註釋

- (void)checkUserAuthentication {
    // 判斷是否實名認證
    if (![YJRuntime sharedInstance].currentUser.tpStatus) {
        // TODO: 去實名認證
        return;
    }
    
    // 判斷是否實設定支付密碼
    if (![YJRuntime sharedInstance].currentUser.payPwdStatus) {
        // TODO: 去設定支付密碼
        return;
    }
    
    // 判斷是否繫結銀行卡
    if (![YJRuntime sharedInstance].currentUser.bankcardStatus) {
        // TODO: 去繫結銀行卡
        return;
    }
    
    // TODO: ...
}

不寫註釋,也能寫出可讀性很強的程式碼:

使用有意義的函式和變數名字

// 如系統的present方法,不管方法名還是變數名都很好的表達了方法的功能和引數的意義
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion

// 有時候我們為了表達清楚某個方法和引數的意思,避免不了會寫的很長
- (void)showWebViewControllerWithProductModel:(YJProductModel *)productModel {
    // ... 
}

區域性變數應該儘量接近使用它的地方

  • 建議的寫法
- (void)loadDataSource {
    ...
    ...
    int index = ...;
    [self refreshViewByIndex:index];
    ...
}
  • 不建議的寫法
- (void)loadDataSource {
    ... 
    int index = ...;
    ...
    ...
    [self refreshViewByIndex:index];
    ...
}

區域性變數名字應該簡潔明瞭

  • 建議的寫法
boolean success = [self deleteFileWithfilePath:...];
if (success) {
    ...
} else {
    ...
}
  • 不建議的寫法
boolean successInDeleteFile = [self deleteFileWithfilePath:...];
if (successInDeleteFile) {
    ...
} else {
    ...
} 

把複雜的邏輯提取出去,做成“幫助函式”

  • 建議的寫法
// 把查詢、更新和儲存的程式碼分開了實現
- (BOOL)updateFileContentWithContent:(NSData *)content filePath:(NSString *)filePath {
    // 1. 呼叫查詢檔案方法
    
    // 2. 更新檔案內容程式碼
    
    // 3. 呼叫儲存檔案方法
}

- (NSData *)findContentWithFilePath:(NSString *)filePath {
    // ...
}

- (BOOL)saveContentWithFilePath:(NSString *)filePath {
    // ...
}

  • 不建議的寫法
// 把查詢、更新和儲存檔案的所有程式碼寫在一塊了
- (BOOL)updateFileContentWithContent:(NSData *)content filePath:(NSString *)filePath {
    // 1. 查詢檔案程式碼
    
    // 2. 更新檔案內容程式碼
    
    // 3. 儲存檔案程式碼
    
    // ...
}

把複雜的表示式提取出去,做成中間變數

  • 建議的寫法
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:@"123" forKey:@"OrderID"];
    [defaults synchronize];
  • 不建議的寫法
[[[NSUserDefaults standardUserDefaults] setObject:@"123" forKey:@"OrderID"] synchronize];

在合理的地方換行

  • 建議的寫法
    if (userName.length > 0 && 
        [phone checkedPhoneNumber] &&
        [password checkedPassword]) {
        // ...
    }
  • 不建議的寫法
    if (userName.length > 0 && [phone checkedPhoneNumber] && [password checkedPassword]) {
        // ...
    }

十五、寫直觀的程式碼

寫程式碼有一條重要的原則:如果有更加直接,更加清晰的寫法,就選擇它(即使它有時候看起來更長,更笨,也一樣選擇它)

  • 建議的寫法
// 案例: 如果需要多個巢狀進行判斷,可以寫成下面那樣,不滿足直接返回的方式
- (void)checkUserAuthentication { 
    if (![YJRuntime sharedInstance].currentUser.tpStatus) {
        // TODO: 去實名認證
        return;
    }
     
    if (![YJRuntime sharedInstance].currentUser.payPwdStatus) {
        // TODO: 去設定支付密碼
        return;
    }
     
    if (![YJRuntime sharedInstance].currentUser.bankcardStatus) {
        // TODO: 去繫結銀行卡
        return;
    }
    
    // TODO: ...
}
  • 不建議的寫法
    if ([YJRuntime sharedInstance].currentUser.tpStatus) {
        if ([YJRuntime sharedInstance].currentUser.payPwdStatus) {
            if ([YJRuntime sharedInstance].currentUser.bankcardStatus) {
            // TODO: ...
            } else {
                // TODO: 去繫結銀行卡
            } 
        } else {
            // TODO: 去設定支付密碼
        } 
    } else {
        // TODO: 去實名認證
    } 

十六、通知及監聽的移除

如果類中監聽了通知,應該在dealloc方法中移除物件監聽

  • 建議的寫法
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
  • 不建議的寫法
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:name1 object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:name2 object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:name3 object:nil];
}

十七、其它

1. 使用readonly修飾,不被外部修改的屬性;

3. 方法命名的規範

  • 如果不是寫初始化方法不要用init進行開頭
  • 如果不是屬性的set方法不要用set作為方法的字首

4. 控制元件命名的規範

  • 一定不要單單用首字母簡寫命名控制元件(特殊意義的除外,如WTO、RMB等),並且名字後一定要加上控制元件型別,例如: UILabel結尾加上Label,UIImageView結尾記上ImageView。
  • 建議的寫法
@property(nonatomic, strong) UILabel *userNameLabel;
  • 不建議的寫法
@property(nonatomic, strong) UILabel *userName;

5. 大括號{}不換行

  • 建議的寫法
if(YES) {
  // ...
}
  • 不建議的寫法
if(YES)
{
  // ...
}

6. 對於#define巨集命名

單詞全部的大寫,且單詞之間用“_”分割,最好加上專案字首

建議的寫法

#define NS_ENUM_AVAILABLE_MAC(_mac) CF_ENUM_AVAILABLE_MAC(_mac)
#define NS_ENUM_AVAILABLE_IOS(_ios) CF_ENUM_AVAILABLE_IOS(_ios)

不建議的寫法

#define ZDScreenHeight [UIScreen mainScreen].bounds.size.height
#define ZDScreenWidth  [UIScreen mainScreen].bounds.size.width

7. 區域性變數

區域性的變數在要初始化時,儘量設定預設值(對於一些物件判斷是否賦值可以不進行初始化)

  • 建議的寫法
int index = 0;
  • 不建議的寫法
int index;

使用駝峰命名法,命名變數

  • 建議的寫法
UIViewController *viewController = [[UIViewController alloc] init];
// 如果實在是 不想寫太長的引數名,可以寫成首字母大寫的形式,但語義一定要清楚
YJWKWebViewController *webVC = [[YJWKWebViewController alloc] initWithUrl:nil];
  • 不建議的寫法
UIViewController *viewcontroller = [[UIViewController alloc] init];
YJWKWebViewController *webvc = [[YJWKWebViewController alloc] initWithUrl:nil];

8. 對於NS_OPTIONS型別多個值用“|”連線不能用“+”

  • 建議的寫法
UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound
  • 不建議的寫法
UNAuthorizationOptionAlert + UNAuthorizationOptionBadge + UNAuthorizationOptionSound

9. block的命名

儘量和蘋果的命名一致使用completion或CompletionHandle結尾,也可用Block命名作為引數名結尾。

  • 建議的寫法
typedef void(DidUpdateViewWithCompletionHandle)()
  • 不建議的寫法
typedef void(DidUpdateViewWithCallBack)()

10. 儘量少在initialize或load方法做一些初始化的事情

  • 建議的做法
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [[UITabBarItem appearance] setTitleTextAttributes:@{NSForegroundColorAttributeName : GBCOLOR(153, 153, 153, 1.0)} forState:UIControlStateNormal];
    [[UITabBarItem appearance] setTitleTextAttributes:                                                         @{NSForegroundColorAttributeName : GBCOLOR(255, 129, 55, 1.0)} forState:UIControlStateSelected];
 }
  • 不建議的做法
+ (void)initialize {
    [[UITabBarItem appearance] setTitleTextAttributes:@{NSForegroundColorAttributeName : GBCOLOR(153, 153, 153, 1.0)} forState:UIControlStateNormal];
    [[UITabBarItem appearance] setTitleTextAttributes:                                                         @{NSForegroundColorAttributeName : GBCOLOR(255, 129, 55, 1.0)} forState:UIControlStateSelected];
}

11. 屬性使用懶載入

  • 建議的寫法
- (UILabel *)titleLabel {
    if (!_titleLabel) {
        _titleLabel               = [[UILabel alloc] init];
        _titleLabel.font         = [UIFont systemFontOfSize:16];
        _titleLabel.textColor     = [UIColor darkGrayColor];
        _titleLabel.textAlignment = NSTextAlignmentCenter;
    }
    return _titleLabel;
}

12. 給controller“瘦身”

  1. 對於頁面的搭建儘量封裝到獨立的view中;

  2. model(或viewModel)負責資料的請求和解析;

  3. controller最好只做頁面的跳轉和互動操作;

  4. 合理利用分類,封裝和擴充套件功能。

13. 多用型別常量,少用#define

  • 建議的寫法
static const NSTimeInterval kAnimationDuration = 0.3;
  • 不建議的寫法
#define ANIMATION_DURATION 0.3

14. 對於一些狀態的判斷,使用列舉表示不同的狀態

儘量少用根據數字來直接判斷不同的狀態

  • 建議的寫法
typedef NS_ENUM(NSUInteger, HomeViewState) {
    HomeViewStateNoData,
    HomeViewStateFailure,
    HomeViewStateItemList,
    HomeViewStateBannerList
};
switch(state) {
    case HomeViewStateNoData : {
        // 顯示沒資料
        break;
    } 
    case HomeViewStateFailure : {
        // 顯示請求錯誤
        break;
    } 
    case HomeViewStateItemList : {
        // 顯示商品的列表
        break;
    } 
    case HomeViewStateBannerList : {
        // 顯示banner列表
        break;
    }
    default :
    break;
} 
  • 不建議的寫法
if(state == 0) {
  // 顯示沒資料
} else if(state == 1) {
  // 顯示請求錯誤
} else if(state == 2) {
  // 顯示商品的列表
} else if(state == 3) {
  // 顯示banner列表
} else {}

15. 對於一些自己不確定的可以使用try catch

對於不知道後臺返回什麼型別的,可以使用try catch (因為OC是執行時語法,可能array不一定是NSArray型別的)

  • 建議的寫法
int index = 0;
@try {
  NSArray *array = obj[@"list"];
  index = [array.firstObject intValue];
}
@catch {}
  • 不建議的寫法
// 如果後臺返回list為欄位 這段程式碼就崩潰了 可以使用try catch也可以用Model庫 或者自己新增判斷
int index = 0;
NSArray *array = obj[@"list"];
if(array.count > 0) {
  index = [array.firstObject intValue];
}

16. 遍歷方法的選擇

  1. 如果只需要便利陣列和字典儘量使用增強for迴圈方法
  • 建議的寫法
for(NSString *name in names) {
  // ...
}
  • 不建議的寫法
for(int i = 0; i < names.lenght ; i ++) {
  NSString *name = names[i];
  // ...
}
  1. 需要便利字典和陣列的內容 並且需要索引用block的遍歷方法
  • 建議的寫法
[names enumerateObjectsUsingBlock:^(NSString * _Nonnull name, NSUInteger idx, BOOL * _Nonnull stop) {
    // ...
}];
  • 不建議的寫法
for(int i = 0; i < names.lenght ; i ++) {
  NSString *name = names[i];
  // ...
}

17. 如果想進行快取優先使用NSCache,不要使用NSDictionary進行快取

  • 建議的寫法
NSCache *cache = [[NSCache alloc] init];
[cache setObject:object forKey:key];
  • 不建議的寫法
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[dictionary setObject:object forKey:key];

18. nil 和 BOOL 檢查

  • 建議的寫法
if(name) {
    // ...
}
if (isMyFriend) {
    // ... 
}
  • 不建議的寫法
if(name != nil) {
    // ...
}
if(isMyFriend == YES) {
    // ... 
}

19. 陣列和字典最好指定元素的型別

  • 建議的寫法
NSArray<NSString *> *names = [NSArray array];
  • 不建議的寫法
NSArray *names = [NSArray array];

20. 陣列和字典的元素垂直寫

  • 建議的寫法
NSArray *array = @[
                    @"a",
                    @"b",
                    @"b"
                    ];
NSDictionary *dictionary = @{
                            @"a" : @"",
                            @"b" : @"",
                            @"c" : @""
                            };
  • 不建議寫法
NSArray *array = @[@"a", @"b", @"b"];
NSDictionary *dictionary = @{@"a" : @"", @"b" : @"", @"c" : @""};

21. 使用CGRect的函式獲取值

  • 建議的寫法
CGFloat x      = CGRectGetMinX(frame);
CGFloat y      = CGRectGetMinY(frame);
CGFloat width  = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);
  • 不建議寫法
CGFloat x      = frame.origin.x;
CGFloat y      = frame.origin.y;
CGFloat width  = frame.size.width;
CGFloat height = frame.size.height;

22. 專案中的git分支功能介紹

  • master:
    • 主分支。只對應線上版本的程式碼。
  • develop:
    • 開發分支,永遠是最新的程式碼,團隊成員需要開始新需求的時候,都是在這個分支上拉取程式碼。
  • fixbug:
    • 修復線上bug的臨時分支。程式碼從master上check下來,上線後merge到master和develop上去,並且會被刪除掉。
  • release:
    • 釋出版本時的臨時分支,從develop分支上check下來,用於固定釋出那一刻的程式碼版本,如果在釋出期間有問題可以在此分支上進行修改。上線後會合併到master和develop上去,並且會被刪除掉。

23. git使用

  • 配置和克隆程式碼
    1. 配置git名稱: git config --global user.name "Bruce Li"
    2. 配置git郵箱: git config --global user.email "xxx@163.com"
    3. 檢視配置資訊: git config -l
    4. 克隆程式碼:git clone "git地址"
  • 程式碼管理
    1. 檢視檔案狀態:git status
    2. 新增所有檔案到暫緩區: git add .
    3. 提交暫緩區的程式碼到本地: git commit -m"提交程式碼說明。"
    4. 用rebase拉去程式碼:git pull origin 分支名稱 --rebase 
    5. 把程式碼退到遠端伺服器:git push origin 分支名稱
  • 分支管理
    1. 檢視本地分支 git branch
    2. 檢視所有分支: git branch -a
    3. 切換分支: git checkout 分支名稱
    4. 建立本地分支: git branch 分支名稱
    5. 把本地分支提交到遠端伺服器: git push origin 分支名稱
    6. 使用rebase合併分支:git rebase 需要和並的分支名稱
    7. 刪除本地分支: git branch -d 分支名稱
    8. 刪除遠端分支: git push origin --delete 分支名稱
  • 標籤(tag)管理
4. 檢視當前的標籤: git tag
5. 打標籤:git tag -a "標籤名" -m"當前標籤的資訊"
6. 把當前標籤推送到遠端: git push origin --tag
7. 刪除本地標籤:git tag -d 標籤名
8. 刪除遠端標籤: git push origin --delete 標籤名

github地址:https://github.com/SilongLi

相關文章