程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Why Java Sucks and C# Rocks(3):Attribute與Annotation

Why Java Sucks and C# Rocks(3):Attribute與Annotation

編輯:關於JAVA

上一篇文章裡我談了Java和C#語言中對於基礎類型的不同態度,我認為C#把基礎類型視做對象的做法比Java更有“萬物皆對象”的理念 ,使用起來也更為方便。此外,C#擁有一個Java 1.4所不存在的特性,即Attribute(自定義特性),而在之後的Java 5.0中也增加了類似 的功能,這便是Annotation(標注)。那麼,Attribute的作用是什麼,Java中的Annotation和C#中的 Attribute又有什麼區別呢,Java 5.0中又從C# 1.0中吸收了哪些優點?我們現在就來關注這方面的問題。

自定義特性與設計

Attribute是C# 1.0中的重要功能,它的作用便是為某個成員,例如類、方法或參數附加上一些元數據,而在程序中則可以通過反射操作 獲取到這些數據。例如,在.NET框架中,每個類型在默認情況下是無法被序列化的,除非我們為類型添加Serializable標記。如下:

[Serializable]
public class Product { ... }

Product類在標記了Serializable之後,就可以被BinarySerializer或 DataContractSerializer等工具類序列化或反序列化。C#有個約 定:所有的Attribute都(直接或間接)繼承 System.Attribute類,並且類名以Attribute結尾,但是在使用時可以省略。因此,事實上 Serializable標記其實是 SerializableAttribute類,它是System.Attribute的子類。

C#中的Attribute對於軟件設計有非常重要的作用,例如Kent Beck評價到:

NUnit 2.0 is an excellent example of idiomatic design. Most folks who port xUnit just transliterate the Smalltalk or Java version. That's what we did with NUnit at first, too. This new version is NUnit as it would have been done had it been done in C# to begin with.

簡而言之,大部分xUnit框架都是簡單地移植JUnit的代碼,但是NUnit卻利用了C#的Attribute提供了更優雅的設計,類似的觀點在 Martin Fowler所編的雜志中也有過更為具體的論述。因此,C#在這方面可謂大大領先於Java 1.4。幸運的是,在C#發布兩年後Java語言也 推出了5.0版本,增加了Annotation功能,這無疑縮小了與C#之間的差距。

只可惜,Java語言中的Annotation功能,我認為相對於C#語言的Attribute功能至少有兩個缺點。

缺點1:失血模型

說起C#的Attribute與Java的Annotation,兩者最大的區別便是:C#中的Attribute是類,而Java中的Annotation是接口。

由於C#的Attribute其實也是.NET中標准的“類”,因此與類有關的設計方式都可以運用其中,例如抽象類,抽象方法,重載方法,也可 以實現接口等等。這類特性造就了一些非常常用的設計模式,例如可能對於大部分.NET程序員都非常熟悉的“驗證標記”。

簡單地說,這是一種通過標記來表示“驗證邏輯”的做法,例如我們可以先定義一個基類:

public class ValidationResult { ... }

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public abstract class ValidationAttribute : Attribute
{
   public abstract ValidationResult Validate(object value);
}

ValidationAttribute類繼承了System.Attribute,也就是說,它可以作為其他Attribute的基類。例如,我們可以定義這樣一些通用的 驗證類:

public class RangeAttribute : ValidationAttribute
{
   public int Min { get; set; }

   public int Max { get; set; }

   public override ValidationResult Validate(object value) { ... }
}

public class RegexAttribute : ValidationAttribute
{
   public string Pattern { get; set; }

   public override ValidationResult Validate(object value) { ... }
}

於是,我們便可以在一個類的屬性上進行標記,表示對某個屬性的驗證要求:

public class Person
{
   [Range(Min = 10, Max = 60)]
   public int Age { get; set; }

   [Range(Min = 30, Max = 50)]
   public int Size { get; set; }

   [Regex(Pattern = @"^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$")]
   public string Email { get; set; }
}

這樣的標記便表示Age的合法范圍是10到60,Size的合法范圍是30到50,而Email需要滿足特定的正則表達式。標記之後,我們便可以使 用統一的代碼進行驗證,例如:

public static void Validate(object o)
{
   var type = o.GetType();
   foreach (var property in type.GetProperties())
   {
     var validateAttrs =
       (ValidationAttribute[])property.GetCustomAttributes(
         typeof(ValidationAttribute), true);

     var propValue = property.GetValue(o, null);
     foreach (var attr in validateAttrs)
     {
       var result = attr.Validate(propValue);
       // do more things
     }
   }
}

如此,我們便可以輕易地獲取每個屬性上的驗證條件,並調用Validate方法進行驗證。我們有能力這麼做的原因,是因為C#中的 Attribute是類,這樣我們可以使用抽象類ValidationAttribute進行統一控制。如果這段驗證邏輯是由類庫提供的,而開發人員想要增加額 外的驗證條件,也只需要自己定義新的類來繼承ValidationAttribute,並提供自定義的Validate方法邏輯即可。這種方式大量出現在各 類.NET的類庫及框架中,給.NET程序員帶來許多便利。

而在Java 5.0中,似乎Annotation和C#的Attribute在表現形式上差不多。例如,我們也可以定義一些“驗證標記”:

public @interface RangeValidation {
   int min();
   int max();
}

public @interface RegexValidation {
   String pattern();
}

使用時似乎也差不多:

public class Person {
   @RangeValidation(min = 10, max = 60)
   public int age;

   @RegexValidation(pattern = "^\\w+@[a-zA-Z_]+?\\.[a-zA-Z]{2,3}$")
   public String email;
}

與C#不同,Java的Annotation其實是接口,它默認實現類庫中定義的java.lang.annotation.Annotation 接口,並且只能定義一些無參 數的方法(不過可以指定默認值)——因為這些方法的作用其實只是類似於一些“字段”,只是用於保存數據,以便在程序中返回。這樣看 來,似乎和C#中沒有區別,只是一個使用了“屬性”,一個利用了“方法”而已,不是嗎?

自然不是,區別很大。試想,現在如果您要編寫一段代碼來進行統一的驗證,那麼該怎麼做?似乎沒法做,因為在C#中我們可以通過統 一的基類來獲取所有表示驗證的Attribute,然後調用定義在基類中的Validate方法。在Java語言中我們無法做到這點,因此如果要識別 RangeValidation,那麼我們就必須獨立准備一段邏輯,而要識別RegexValidation則又是另一段不同的方法。因為兩者的“識別” 方式完 全不同,因此我們無法使用相同的代碼進行驗證工作。

更關鍵的是,如果驗證邏輯是由類庫提供的,而開發人員想要補充額外的驗證方式,那麼該怎麼做?我們可以提供自定義的Annotation ,這很容易,那麼識別這個Annotation的邏輯該如何交給類庫呢?這個自然也有辦法解決,但無論如何都會帶來較多的代碼量,且做不到C# 般優雅,自然。

因此,Java的Annotation落後於C#的Attribute的關鍵之處,在於Java的Annotation只能定義為失血的對象,而C#的Attribute可以在需 要的時候包含一定的邏輯,這樣便可以讓C#程序員獲得更好的靈活性,使用更豐富的開發模式。與此相比,“類”和“接口”的區別,其實 倒真只是表象罷了。

缺點2:古怪的約定

相比於上一個缺點來說,第二個缺點似乎並不那麼嚴重,不過我認為這的確也是Java語言的Annotation設計中無法令人滿意的地方。

在前面的代碼中我們已經可以發現,其實C#的Attribute及Java的Annotation在使用上非常相似,為此我們再來仔細對比一下:

[Range(Min = 10, Max = 60)] // C#

@RangeValidation(min = 10, max = 60) // Java

這樣看來,C#和Java在使用時的形式基本完全一致,都是使用名稱+屬性名的方式進行標記。不過其實C#和Java都有額外的語法,例如在 C#中,我們可以這樣定義RangeAttribute類:

public class RangeAttribute : ValidationAttribute
{
   public RangeAttribute() { }

   public RangeAttribute(int min, int max)
   {
     this.Min = min;
     this.Max = max;
   }

   public int Min { get; set; }

   public int Max { get; set; }

   ...
}

與之前的RangeAttribute相比,新的定義增加了兩個構造函數定義,一個是無參數的構造函數(其實原來的定義也有,只不過由編譯器 自動添加),還有一個構造函數則直接接收min和max參數,這樣我們便可以直接通過構造函數來標記Attribute了:

[Range(10, 60)]

當然,有人說這種做法不如顯式指定Min和Max來的清晰——可能是這樣吧,但是在很多時候通過構造函數傳參也有優勢,例如我們可以 重新定義之前的RegexAttribute為:

public class RegexAttribute : ValidationAttribute
{
   public RegexAttribute(string pattern)
   {
     this.Pattern = pattern;
   }

   public string Pattern { get; private set; }

   ...
}

在這裡我們為RegexAttribute提供了一個接收pattern的構造函數,並將Pattern屬性的set方法設為private。於是我們便可以這樣使用 RegexAttribute了:

[Regex(@"^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$")]

這麼做有兩個好處:首先,外部無法設置Pattern屬性的值,這點加強了對象的封裝性。其次,這裡相當於強制RegexAttribute在使用時 必須提供一個pattern參數(雖然無法進行驗證)。這樣,代碼在使用時既顯得優雅,可讀性也非常良好。但其實,我認為更關鍵的是,這 種使用模式和創建一個對象,並為其屬性進行賦值一樣:我們可以選擇任意的構造函數創建對象,再“有選擇地”進行屬性賦值。例如上文 AttributeUsage的使用:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]

這裡的含義是,創建一個AttributeUsageAttribute類,提供AttributeTargets.Property作為構造函數的參數,並同時將AllowMultiple 屬性設為true。可見,這種做法給了我們相當自由性——而且非常自然,沒有奇怪約定。在設計一個 Attribute的時候,我們一般可以提供 幾個常用的構造函數,在大部分情況下使用這些構造函數也已經足夠了。除此之外,Attribute對象的屬性包含了默認值,在必要的時候可 以進行修改。

在Java語言中,事實上我們也可以修改RegexValidation類,讓開發人員可以通過這種方式來使用:

@RegexValidation("^\\w+@[a-zA-Z_]+?\\.[a-zA-Z]{2,3}$")

只要我們可以把RegexValidation改成這樣即可:

public @interface RegexValidation {
   String value();
}

在Java語言中,假如Attribute的一個成員名為value,且沒有其他成員(或是其他成員都提供了默認值),那麼便可以如C#中構造函數 般使用。那麼……

如果我想使用“構造函數”的方式傳遞數據,但成員名還是想用pattern,可以嗎?

如果我想使用“構造函數”的方式傳遞多個參數,可以嗎?

不可以。我也不知道為什麼會有這種特別的約定規則呢?可能和Annotation是接口有關吧。接口沒有構造函數,因此沒有一個合適的方 式以省去成員名的方式傳遞參數,只能指定一個特別的名稱了——但對於開發人員來說,憑什麼我的Regex值一定要叫value而不能叫 pattern呢?

Java 5.0學習C#

我不知道為什麼有了C#的優秀榜樣,Java 5.0卻還是不願意做的更好。其實C#中的Attribute也有缺點,例如無法使用泛型,所以Java完 全有勝過C#的機會。其實在以後的文章中您也可以發現這樣一個現象:C#的榜樣並不完美,但Java的進化更為糟糕。對此,我們除了一聲長 歎又能怎麼辦呢?

除了Annotation之外,Java 5.0還從C#處學習了以下功能:

可以使用enum關鍵字定義強類型的常量——C# 1.0中也有類似功能(好吧,我承認,其實Java的enum功能比C#中要豐富一些)。

可變參數,即可以使用“一一列舉”形式,提供某個方法最後一個數組參數的內容——其實就是C# 1.0中的params。

增強了for的能力,可以方便地使用枚舉器(Iterator)——這其實就是C# 1.0中的foreach。

那麼,到底是誰是所謂的“copy cat”呢?就像我在第一篇文章中寫的那樣,“自從C# 1.0誕生之日起,就只出現Java借鑒C#特性的情 況,至今已將近10年”,以後我們還可以看到更多例子。我並不想說所謂的“抄襲”,我只想說“學習” 或是“借鑒”。我認為,只要是 優點,出現雷同這都是完全正常且值得鼓勵的。我現在提到這些,主要的目的是想告訴那些固執地認為“C#只是Java的山寨復制品”的同學 們一個事實。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved