iT邦幫忙

2024 iThome 鐵人賽

DAY 26
0

今天想來聊較特別的議題,以前寫程式,對var的使用非常習慣。但蠻多人對於var使用非常感冒,因為對於型別不明確這件事情會大大降低閱讀性。我的思維覺得如果var真的這麼母湯,java與Net這兩種強行別語言應該就不會納入var的使用…所以特別對這部分去做一個較深入的整理。(感覺我的主題應該要換成後端與開發大小事亂聊了..)。這章節主要是要談寫var這件事情,跟Quarkus框架較無關係,概念偏向用在撰寫程式碼上。

一、簡述var

var 關鍵字用於隱式型別聲明。簡單來說,當你使用 var,你告訴編譯器去自動判斷變數的型別。舉例來說:

var i = 10;  // 自動判斷為 int
var s = "hello";  // 自動判斷為 string

為什麼我們不寫明確的Type,要使用var? var在一開始的語言設計主要為了簡化代碼和提高可讀性,另外在一些語法糖上,可以讓開發者在撰寫語法上可以更簡便。這樣描述還是會太含糊,下述會針對幾個情境去探討使用var的適用性。

二、探討使用情境

此章節會探討var的適用與不適用情境去

a、簡潔性

假設你正在使用一個很長、很複雜的型別名稱,像是 Map<Integer, List<String>>。如果每次宣告這個型別都需要完全寫出來,那會讓代碼看起來很雜亂,也浪費你的時間。又或是很長的物件宣告,例如Google Cloud的KMS Package KeyManagementServiceClient 物件。

I.好的使用情境(Java Stream API)

關於複雜型別,舉個例子如下,我們 Java Stream API為例子會很好使看出他的優勢,假設你有一個名為 users 的列表,每個元素都是一個 User 類別的實例,你想要選出所有年齡大於 20 的用戶。

如果不使用 var,你需要這樣寫:

List<User> usersAbove20 = users.stream()
    .filter(u -> u.getAge() > 20)
    .collect(Collectors.toList());

但假如你的查詢更複雜,回傳的型別也會更複雜。例如,你想從這些用戶中取出他們的姓名和年齡,然後排序。這時,你可能需要寫:

List<UserSummary> result = users.stream()
    .filter(u -> u.getAge() > 20)  // Where
    .map(u -> new UserSummary(u.getName(), u.getAge()))  // Select
    .sorted(Comparator.comparing(UserSummary::getAge))  // OrderBy
    .collect(Collectors.toList());  // Collect to List

相對地,使用 var 會讓這一切變得簡單許多:

var result = users.stream()
    .filter(u -> u.getAge() > 20)
    .map(u -> new UserSummary(u.getName(), u.getAge()))
    .sorted(Comparator.comparing(UserSummary::getAge))
    .collect(Collectors.toList());

匿名型別

接著稍微提一下匿名型別,Java沒有匿名型別,這邊以C#語法闡述匿名型別,以上述例子C#語法會如下

 var result = users
                .Where(u => u.Age > 20)  // 篩選年齡大於20的用戶
                .Select(u => new { Name = u.Name, Age = u.Age })  // 選取姓名和年齡,使用匿名型別
                .OrderBy(u => u.Age)  // 按年齡排序
                .ToList();  // 收集結果到List

在這串程式碼,他會是一個匿名型別如下

image.png

匿名型別是一種沒有顯示名稱的型別。這種型別主要用於儲存一些臨時的、結構簡單的資料。當你需要從一個複雜的資料結構中提取特定的資訊,但又不想定義一個全新的類別。通常就會使用var,操作起來會相對於簡單很多。這段重點在於資料過渡階段的操作,尤其是當你不需要長期存儲或多次使用某個特定結構的資料時,使用 var 和匿名型別是很好的選擇。

再舉一個例子,一個任務是對來自多個來源(資料庫、APIs、用戶輸入等)的資料進行綜合處理。你需要用到多個類型、集合、使用var Lambda 表達式如下

var query = dataContext.Customers
              .Where(c => c.Age > 18)
              .SelectMany(c => c.Orders, (c, o) => new { c.Name, o.OrderId, o.Amount })
              .GroupBy(x => x.Name)
              .Select(g => new { Customer = g.Key, TotalAmount = g.Sum(x => x.Amount) });

如果不使用var會如下

IEnumerable<IGrouping<string, anonymousType<string, int, decimal>>> query = 
      dataContext.Customers
                 .Where(c => c.Age > 18)
                 .SelectMany(c => c.Orders, (c, o) => new { c.Name, o.OrderId, o.Amount })
                 .GroupBy(x => x.Name)
                 .Select(g => new { Customer = g.Key, TotalAmount = g.Sum(x => x.Amount) });

在這個例子中,不使用 var 使得類型非常明確,但代價是代碼變得過於冗長和難以閱讀。

II.好的使用情境(過長的Object命名)

Lambda語法糖在很多語言其實都有,另外一個var是用情境在撰寫flow過程中,蠻常遇到一些很長的Object命名,舉一個Google Cloud KMS C#封裝非對稱解密方法如下

過程中,可以看到物件的命名規則,我們可以很清楚理解整個流程為

  • 建立keyManagementServiceClient
  • 產生cryptoKeyVersionName
  • decode cryptoKeyVersionName 為bytes
  • 開始作非對稱解密
  • 回傳結果
@Override
  public String asymmetricDecrypt(KeyInfoDto keyInfo, String ciphertext) throws IOException {
    try (KeyManagementServiceClient keyManagementServiceClient = KeyManagementServiceClient.create()) {

      // 調用CreateCryptoKeyVersionName方法,根據keyInfo生成密鑰版本名稱
      CryptoKeyVersionName cryptoKeyVersionName  = generateCryptoKeyVersionName(keyInfo);

      // 將加密後的文本(Base64)轉換為字節數組
      byte[] decryptedBytes = decodeBase64ToBytes(ciphertext);

      // 使用密鑰版本名稱進行非對稱解密
      AsymmetricDecryptResponse asymmetricDecryptResponse = keyManagementServiceClient.asymmetricDecrypt(cryptoKeyVersionName, ByteString.copyFrom(decryptedBytes));

      // 將解密後的字節數組轉換為UTF-8字符串並返回
      return asymmetricDecryptResponse.getPlaintext().toStringUtf8();
    }
  }

如果我們把程式碼改成使用var如下,此時可以問自己,有影響可讀性嗎? 其實沒有,相對來說,對於宣告Type物件,看起來會簡潔一點,但重點在Naming有定義清楚狀況下,閱讀起來不會讓長的物件命名礙到眼,除非你flow某個段落不太清楚想去理解他的型態,此時我們就可以透過IDE協助滑鼠移過去理解。

@Override
  public String asymmetricDecrypt(KeyInfoDto keyInfo, String ciphertext) throws IOException {
    try (var keyManagementServiceClient = KeyManagementServiceClient.create()) {

      // 調用CreateCryptoKeyVersionName方法,根據keyInfo生成密鑰版本名稱
      var cryptoKeyVersionName  = generateCryptoKeyVersionName(keyInfo);

      // 將加密後的文本(Base64)轉換為字節數組
      byte[] decryptedBytes = decodeBase64ToBytes(ciphertext);

      // 使用密鑰版本名稱進行非對稱解密
      var asymmetricDecryptResponse = keyManagementServiceClient.asymmetricDecrypt(cryptoKeyVersionName, ByteString.copyFrom(decryptedBytes));

      // 將解密後的字節數組轉換為UTF-8字符串並返回
      return asymmetricDecryptResponse.getPlaintext().toStringUtf8();
    }
  }

我覺得上述例子感受度還不夠強烈,我在用一段程式碼來比較如下,這段在於對RSA加密,使用指定的DER格式公鑰(derKeyBytes)來完成整個過程,我們直接使用var

  1. 讀取公鑰x509EncodedKeySpec = new X509EncodedKeySpec(derKeyBytes); 這行用於讀取DER格式的公鑰數據。
  2. 生成RSA公鑰對象(rsaPublicKey):接下來,用這些公鑰數據來生成一個實際的RSA公鑰對象。
  3. 初始化加密器:選用了一種特殊的RSA加密模式和填充方式,這是由**Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");**這行確定的。
  4. 設置參數:在這個例子中,使用了OAEP(光學端對端加密填充)和SHA-256作為Hash算法。
  5. 加密動作:最後,**rsaCipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));**這行將明文轉換為字節數組並進行加密。
  6. 返回結果:加密後的數據會被轉換成Base64格式的字串,然後返回。

整個過程可以感受到,在命名有命好狀況下,其實完全不影響整個易讀性。甚至簡化冗長的物件名稱宣告,讓程式碼寬度不至於太長很難閱讀。

/**
   * 進行RSA加密使用指定的DER格式公鑰。
   *
   * @param plaintext    需要加密的明文。
   * @param derKeyBytes  DER格式的公鑰位元組。
   * @return             返回加密後的Base64字串。
   * @throws InvalidKeySpecException  如果加密過程中出現任何安全相關的異常。
   */
  private String encryptUsingPublicKey(String plaintext, byte[] derKeyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
    var x509EncodedKeySpec = new X509EncodedKeySpec(derKeyBytes);

    // 生成RSA公鑰對象
    var rsaPublicKey  = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec);

    // 初始化加密器和相關參數
    var rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
    var oaepParameters = new OAEPParameterSpec("SHA-256", "MGF1"
            , MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
    rsaCipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey , oaepParameters );

    // 進行加密
    byte[] encryptedData  = rsaCipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
    return encodeBase64ToString(encryptedData);
  }

以下為比照組,使用強型別type,我們再來讀一段就會有感覺相對上述段還來的些微不易閱讀

/**
   * 進行RSA加密使用指定的DER格式公鑰。
   *
   * @param plaintext    需要加密的明文。
   * @param derKeyBytes  DER格式的公鑰位元組。
   * @return             返回加密後的Base64字串。
   * @throws InvalidKeySpecException  如果加密過程中出現任何安全相關的異常。
   */
  private String encryptUsingPublicKey(String plaintext, byte[] derKeyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(derKeyBytes);

    // 生成RSA公鑰對象
    PublicKey rsaPublicKey  = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec);

    // 初始化加密器和相關參數
    Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
    OAEPParameterSpec oaepParameters = new OAEPParameterSpec("SHA-256", "MGF1"
            , MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT);
    rsaCipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey , oaepParameters );

    // 進行加密
    byte[] encryptedData  = rsaCipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
    return encodeBase64ToString(encryptedData);
  }

III. 不好的使用情境

聊完對於簡潔性好處外,其實var也有不適當的應用情境。我們直接帶Context情境去了解,請看下述測試段程式碼,下段程式碼為一個對稱與非對稱的API測試,我們要比較encrypt 與 decrypt 。此時如果我們使用var,我們會不清楚們比較的是什麼…String? 或是 byte[] ?

在這個很需要資料明確定義型態的情境,就不適合使用var去做宣告

String testPlaintext ="PAL";
// 設置金鑰訊息
var keyInfoDto = new KeyInfoDto();
keyInfoDto.setProjectId("affable-cacao-389805");
keyInfoDto.setLocationId("asia-east1");
keyInfoDto.setKeyRingId("cathy-sample-project");
keyInfoDto.setKeyVersion("1");

// 測試對稱非對稱 (明文加密->解密->比對)
keyInfoDto.setKeyId("kms-sdk-testing");
// 對稱加密
var encrypt = kmsService.symmetricEncrypt(keyInfoDto,testPlaintext);
// 對稱解密
var decrypt = kmsService.symmetricDecrypt(keyInfoDto,encrypt);
// 解密後比對明文
Assert.hasText(decrypt,testPlaintext);

所以確實在某些狀況,var的宣告不太適當,例如

  • 跨平台或多語言整合

    • 如果你的後端微服務需要與其他平台或用不同語言寫的服務進行交互,明確的型別會更容易讓其他開發者理解代碼。

      // 不適合用 var,因為其他開發者或平台需要知道明確的型別
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      
  • 容易混淆的型別

    • 當你處理兩個或多個類似但不完全相同的型別時,使用 var 可能會造成混淆。
    // 明確型別可以避免將 BigDecimal 與 BigInteger 混淆
    BigDecimal bigDecimal = new BigDecimal("10.0");
    BigInteger bigInteger = new BigInteger("10");
    
  • 數學運算

    • 當你進行數學運算時,使用 var 也可能會引發問題。例如:
    // 是 int 還是 double?
    var result = 10 / 2;
    
  • 初始化值不清晰

    • 當初始化值不提供足夠的型別信息時,使用 var 會讓型別變得不明確。
    // 到底是 List<String> 還是 ArrayList<String>,或是其他?
    var names = Arrays.asList("Alice", "Bob");
    
  • 單元測試與資料驗證(包含上述比較encrypt 與 decrypt例子)

    • 在單元測試中,你經常需要確保變數的型別與預期相符。使用明確的型別可以提供更多的上下文信息。

      // 測試情境中,我們想要確保 getResult 返回的是一個 List
      List<String> result = someObject.getResult();
      
  • foreach寫法

    • 如果使用**foreach迴圈,並且集合的項目型別不是很明確,那麼使用var**可能會降低程式碼的可讀性。

      List<DetailedOrder> orders = GetOrders();
      
      // 使用明確型別
      foreach (DetailedOrder order in orders)
      {
          // ...
      }
      
      // 使用 var
      foreach (var order in orders)
      {
          // ...
      }
      

因此在需要明確資料型態以增加代碼可讀性和維護性的場合,特別是涉及跨平台交互、容易混淆的型別、數學運算、不明確的初始化值,以及單元測試與資料驗證時,使用 var 往往不是最佳選擇。

b、鼓勵描述性命名(有意義命名)

當你使用 var 來宣告變數時,因為變數的型別不是明確寫在代碼中,所以為了讓讀取代碼的人(或者是你自己在未來能夠更快速地理解這個變數是用來做什麼的,你會被鼓勵給變數一個更具描述性的名字。

假設你不使用 var,而是使用明確的型別名稱來宣告變數:

List<Student> list = new List<Student>();

在這裡,你可能簡單地命名變數為 list,因為型別 List<Student> 已經告訴你這是一個學生列表。

但如果你使用 var

var students = new List<Student>();

這時,你可能會選擇一個更具描述性的名稱 students,來清晰地表明這個變數是用來存儲學生的。這樣,即使型別不是直接寫在代碼中,但從變數名稱 students,我們仍然可以很快地了解它的用途。

不過這部分,我覺得實際也很看開發者Clean Code Sense,如果一個連Clean Code都不懂的其實用var就會是一件很災難的事情。

三、簡易總結條例

  • 代碼簡潔性:使用 var 可以使代碼更簡潔,尤其是當處理長型別名稱時。
  • 型別推斷:對於匿名型別(如 Linq 查詢結果),必須使用 var
  • 讀取性:在某些情境下,避免重複的型別名稱可以提高代碼的讀取性。
  • 情境不明確的型別:如果情境需求初始化表達式不清楚,使用 var 可能會導致讀者不知道變數的實際型別。
  • 過度使用:呼應上一點,不考慮情境過度使用 var 可能會使代碼難以維護和理解。

使用 var 可以提高代碼的可讀性和靈活性,特別是在需要處理匿名或動態型別時。當然,開發者應該根據具體情況來決定是否使用 var,並確保代碼的意圖仍然清晰。


上一篇
開發觀念建置-微服務中的資料庫權限管理:服務即使用者
下一篇
開發概念建置- 微服務監控設計大小事
系列文
微服務奇兵:30天Quarkus特訓營30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言