文章出處

寫在前面

  關于控制反轉(Inversion of Control)和依賴注入(Dependency Injection)大家網上可以找下相關概念,在小菜學習設計模式(五)—控制反轉(Ioc)》這篇文章中本人也有詳細的解釋,這邊再說明下,有很多人把控制反轉和依賴注入混為一談,雖然在某種意義上來看他們是一體的,但好像又有些不同,就比如在上篇文章中所提到的示例。控制反轉(Ioc)可以看成自來水廠,那自來水廠的運行就可以看作依賴注入(DI),Ioc是一個控制容器,DI就是這個容器的運行機制,有點像國家主席和總理的意思。

  關于Ioc的框架有很多,比如astle Windsor、Unity、Spring.NET、StructureMap,我們這邊使用微軟提供的Unity做示例,你可以使用Nuget添加Unity,也可以引用Microsoft.Practices.Unity.dll和Microsoft.Practices.Unity.Configuration.dll,下面我們就一步一步的學習下Unity依賴注入的詳細使用。

  內容有點多,請堅持往下看哦!

構造器注入

構造器注入(Constructor Injection):IoC容器會智能地選擇選擇和調用適合的構造函數以創建依賴的對象。如果被選擇的構造函數具有相應的參數,IoC容器在調用構造函數之前解析注冊的依賴關系并自行獲得相應參數對象。

  通過上面的定義看以看出,使用構造器注入需要在在構造函數中傳遞一個抽象參數,Ioc會自動解析具象所依賴的抽象并注冊給具象,我們還是用上篇喝水作為示例:

 1 /// <summary>
 2         /// 人接口
 3         /// </summary>
 4         public interface IPeople
 5         {
 6             void DrinkWater();
 7         }
 8         /// <summary>
 9         /// 村民
10         /// </summary>
11         public class VillagePeople : IPeople
12         {
13             IWaterTool _pw;
14             public VillagePeople(IWaterTool pw)
15             {
16                 _pw = pw;
17             }
18             public void DrinkWater()
19             {
20                 Console.WriteLine(_pw.returnWater());
21             }
22         }
23         /// <summary>
24         /// 壓水井
25         /// </summary>
26         public class PressWater : IWaterTool
27         {
28             public string returnWater()
29             {
30                 return "地下水好甜啊!!!";
31             }
32         }
33         /// <summary>
34         /// 獲取水方式接口
35         /// </summary>
36         public interface IWaterTool
37         {
38             string returnWater();
39         }
View Code

  代碼很簡單,PressWater依賴于IWaterToolVillagePeople構造函數中傳遞一個IWaterTool的抽象,我們看下調用代碼:

1         static void Main(string[] args)
2         {
3             UnityContainer container = new UnityContainer();//創建容器
4             container.RegisterType<Test01.IWaterTool, Test01.PressWater>();//注冊依賴對象
5             Test01.IPeople people = container.Resolve<Test01.VillagePeople>();//返回調用者
6             people.DrinkWater();//喝水
7         }

  運行結果:

  上面主要用到Unity的RegisterType和Resolve的泛型方法,我們看下RegisterType方法簽名:

 1         //
 2         // 摘要:
 3         //     Register a type mapping with the container.
 4         //
 5         // 參數:
 6         //   container:
 7         //     Container to configure.
 8         //
 9         //   injectionMembers:
10         //     Injection configuration objects.
11         //
12         // 類型參數:
13         //   TFrom:
14         //     System.Type that will be requested.
15         //
16         //   TTo:
17         //     System.Type that will actually be returned.
18         //
19         // 返回結果:
20         //     The Microsoft.Practices.Unity.UnityContainer object that this method was
21         //     called on (this in C#, Me in Visual Basic).
22         //
23         // 備注:
24         //      This method is used to tell the container that when asked for type TFrom,
25         //     actually return an instance of type TTo. This is very useful for getting
26         //     instances of interfaces.
27         //     This overload registers a default mapping and transient lifetime.
28         public static IUnityContainer RegisterType<TFrom, TTo>(this IUnityContainer container, params InjectionMember[] injectionMembers) where TTo : TFrom;
View Code

  我們可以看到RegisterType的第一個參數是this IUnityContainer container,我們上面調用的時候并沒有傳遞一個IUnityContainer 類型的參數,為什么這里會有一個this關鍵字,做什么用?其實這就是擴展方法。這個擴展方法在靜態類中聲明,定義一個靜態方法(UnityContainerExtensions類和RegisterType都是靜態的),其中第一個參數定義可它的擴展類型。RegisterType方法擴展了UnityContainerExtensions類,因為它的第一個參數定義了IUnityContainer(UnityContainerExtensions的抽象接口)類型,為了區分擴展方法和一般的靜態方法,擴展方法還需要給第一個參數使用this關鍵字。

  還有就是RegisterType的泛型約束 where TTo : TFrom; TTo必須是TFrom的派生類,就是說TTo依賴于TFrom

  我們再來看下Resolve泛型方法的簽名:

 1         //
 2         // 摘要:
 3         //     Resolve an instance of the default requested type from the container.
 4         //
 5         // 參數:
 6         //   container:
 7         //     Container to resolve from.
 8         //
 9         //   overrides:
10         //     Any overrides for the resolve call.
11         //
12         // 類型參數:
13         //   T:
14         //     System.Type of object to get from the container.
15         //
16         // 返回結果:
17         //     The retrieved object.
18         public static T Resolve<T>(this IUnityContainer container, params ResolverOverride[] overrides);
View Code

  “Resolve an instance of the default requested type from the container”,這句話可以翻譯為:解決從容器的默認請求的類型的實例,就是獲取調用者的對象。

  關于RegisterType和Resolve我們可以用自來水廠的例子來說明,請看下面:

  • RegisterType:可以看做是自來水廠決定用什么作為水源,可以是水庫或是地下水,我只要“注冊”開關一下就行了。
  • Resolve:可以看做是自來水廠要輸送水的對象,可以是農村或是城市,我只要“控制”輸出就行了。

Dependency屬性注入

屬性注入(Property Injection):如果需要使用到被依賴對象的某個屬性,在被依賴對象被創建之后,IoC容器會自動初始化該屬性。

  屬性注入只需要在屬性字段前面加[Dependency]標記就行了,如下:

 1         /// <summary>
 2         /// 村民
 3         /// </summary>
 4         public class VillagePeople : IPeople
 5         {
 6             [Dependency]
 7             public IWaterTool _pw { get; set; }
 8             public void DrinkWater()
 9             {
10                 Console.WriteLine(_pw.returnWater());
11             }
12         }

  調用方式和構造器注入一樣,通過RegisterType<Test02.IWaterTool, Test02.PressWater>();注入就可以了,除了使用RegisterType方法注冊,我們還可以在配置文件中注冊,[Dependency]和RegisterType方式其實都會產生耦合度,我們要添加一個屬性或是修改一中注冊都會去修改代碼,我們要做的就是代碼不去修改,只要修改配置文件了,這個在下面有講解,這邊就不多說,我們先看下使用UnityConfigurationSection的Configure方法加載配置文件注冊:

1   <unity>
2     <containers>
3       <container name="defaultContainer">
4         <register type="UnityContainerDemo.IWaterTool,UnityContainerDemo" mapTo="UnityContainerDemo.PressWater,UnityContainerDemo"/>
5         <register type="UnityContainerDemo.IPeople,UnityContainerDemo" mapTo="UnityContainerDemo.VillagePeople02,UnityContainerDemo"/>
6       </container>
7     </containers>
8   </unity>

  調用代碼:

1         public static void FuTest02()
2         {
3             UnityContainer container = new UnityContainer();//創建容器
4             UnityConfigurationSection configuration = (UnityConfigurationSection)ConfigurationManager.GetSection(UnityConfigurationSection.SectionName);
5             configuration.Configure(container, "defaultContainer");
6             IPeople people = container.Resolve<IPeople>();//返回調用者
7             people.DrinkWater();//喝水
8         }

  運行結果:

InjectionMethod方法注入

方法注入(Method Injection):如果被依賴對象需要調用某個方法進行相應的初始化,在該對象創建之后,IoC容器會自動調用該方法。

  方法注入和屬性方式使用一樣,方法注入只需要在方法前加[InjectionMethod]標記就行了,從方法注入的定義上看,只是模糊的說對某個方法注入,并沒有說明這個方法所依賴的對象注入,所依賴的對象無非就三種:參數、返回值和方法內部對象引用,我們做一個示例試下:

 1     /// <summary>
 2     /// 村民
 3     /// </summary>
 4     public class VillagePeople03 : IPeople
 5     {
 6         public IWaterTool tool;//我是對象引用
 7         public IWaterTool tool2;//我是參數
 8         public IWaterTool tool3;//我是返回值
 9         [InjectionMethod]
10         public void DrinkWater()
11         {
12             if (tool == null)
13             { }
14         }
15         [InjectionMethod]
16         public void DrinkWater2(IWaterTool tool2)
17         {
18             this.tool2 = tool2;
19         }
20         [InjectionMethod]
21         public IWaterTool DrinkWater3()
22         {
23             return tool3;
24         }
25     }

  調用代碼:

 1         public static void FuTest03()
 2         {
 3             UnityContainer container = new UnityContainer();//創建容器
 4             UnityConfigurationSection configuration = (UnityConfigurationSection)ConfigurationManager.GetSection(UnityConfigurationSection.SectionName);
 5             configuration.Configure(container, "defaultContainer");
 6             VillagePeople03 people = container.Resolve<IPeople>() as VillagePeople03;//返回調用者
 7             Console.WriteLine("people.tool == null(引用) ? {0}", people.tool == null ? "Yes" : "No");
 8             Console.WriteLine("people.tool2 == null(參數) ? {0}", people.tool2 == null ? "Yes" : "No");
 9             Console.WriteLine("people.tool3 == null(返回值) ? {0}", people.tool3 == null ? "Yes" : "No");
10         }

  container.Resolve<IPeople>() as VillagePeople03;其實多此一舉,因為已經在配置文件注冊過了,不需要再進行轉化,這邊只是轉化只是方便訪問VillagePeople03對象的幾個屬性值,我們看下運行效果:

  結果不言而喻,其實我們理解的方法注入就是對參數對象的注入,從typeConfig節點-method節點-param節點就可以看出來只有參數的配置,而并沒有其他的配置,關于typeConfig下面會講到。

非泛型注入

  除了我們上面使用RegisterType和Resolve泛型方法,我們也可以使用非泛型注入,代碼如下:

1         public static void FuTest04()
2         {
3             UnityContainer container = new UnityContainer();//創建容器
4             container.RegisterType(typeof(IWaterTool), typeof(PressWater));//注冊依賴對象
5             IPeople people = (IPeople)container.Resolve(typeof(VillagePeople01));//返回調用者
6             people.DrinkWater();//喝水
7         }

  運行效果:

標識鍵

  我們知道,Unity提供了對象的容器,那么這個容器是如何進行索引的呢?也就是說,容器內的單元是如何標識的呢?在Unity中,標識主要有兩種方式, 一種是直接使用接口(或者基類)作為標識鍵,另一種是使用接口(或者基類)與名稱的組合作為標識鍵,鍵對應的值就是具體類。

  第一種使用接口(或者基類)作為標識鍵:

1 container.RegisterType<IWaterTool, PressWater>();

  代碼中的IWaterTool就是作為標識鍵,你可以可以使用基類或是抽象類作為標示,獲取注冊對象:container.Resolve<IWaterTool>(),如果一個Ioc容器容器里面注冊了多個接口或是基類標示,我們再這樣獲取就不知道注冊的是哪一個?怎么解決,就是用接口或是基類與名稱作為標識鍵,示例代碼如下:

1         public static void FuTest05()
2         {
3             UnityContainer container = new UnityContainer();//創建容器
4             container.RegisterType<IWaterTool, PressWater>("WaterTool1");//注冊依賴對象WaterTool1
5             container.RegisterType<IWaterTool, PressWater>("WaterTool2");//注冊依賴對象WaterTool2
6             IWaterTool wt = container.Resolve<IWaterTool>("WaterTool1");//返回依賴對象WaterTool1
7             var list = container.ResolveAll<IWaterTool>();//返回所有注冊類型為IWaterTool的對象
8         }

  我們只需要在泛型方法RegisterType傳入一個名稱就可以來區分了(和注冊接口或基類),獲取的話也只要傳入注冊時候的名稱即可,我們看下list中的集合對象:

ContainerControlledLifetimeManager單例

  關于單例概念可以參考小菜學習設計模式(二)—單例(Singleton)模式》這篇文章為了實現單例模式,我們通常的做法是,在類中定義一個方法如GetInstance,判斷如果實例為null則新建一個實例,否則就返回已有實例。但是我覺得這種做法將對象的生命周期管理與類本身耦合在了一起。所以我覺得遇到需要使用單例的地方,應該將生命周期管理的職責轉移到對象容器Ioc上,而我們的類依然是一個干凈的類,使用Unity創建單例代碼:

1         public static void FuTest07()
2         {
3             UnityContainer container = new UnityContainer();//創建容器
4             container.RegisterType<IWaterTool, PressWater>(new ContainerControlledLifetimeManager());//注冊依賴對象
5             IPeople people = container.Resolve<VillagePeople01>();//返回調用者
6             people.DrinkWater();//喝水
7         }

  上面演示了將IWaterTool注冊為PressWater,并聲明為單例,ContainerControlledLifetimeManager字面意思上就是Ioc容器管理聲明周期,我們也可以不使用類型映射,將某個類注冊為單例:

1 container.RegisterType<PressWater>(new ContainerControlledLifetimeManager());

  除了將類型注冊為單例,我們也可以將已有對象注冊為單例,使用RegisterInstance方法,示例代碼:

1 PressWater pw = new PressWater();
2 container.RegisterInstance<IWaterTool>(pw);

  上面的代碼就表示將PressWater的pw對象注冊到Ioc容器中,并聲明為單例。

  如果我們在注冊類型的時候沒有指定ContainerControlledLifetimeManager對象,Resolve獲取的對象的生命周期是短暫的,Ioc容器并不會保存獲取對象的引用,就是說我們再次Resolve獲取對象的時候,獲取的是一個全新的對象,如果我們指定ContainerControlledLifetimeManager,類型注冊后,我們再次Resolve獲取的對象就是上次創建的對象,而不是再重新創建對象,這也就是單例的意思。

Unity注冊配置問題

  一開始我做的這示例是類類注冊,就是說類包含類,所有的接口和對象都是放在TestXX類文件中,在配置register注冊節點的時候,總是報下面錯誤:

或者

  Unity配置文件:

1   <unity>
2     <containers>
3       <container name="defaultContainer">
4         <register type="UnityContainerDemo.Test02.IWaterTool,UnityContainerDemo" mapTo="UnityContainerDemo.Test02.PressWater,UnityContainerDemo"/>
5         <register type="UnityContainerDemo.Test02.IPeople,UnityContainerDemo" mapTo="UnityContainerDemo.Test02.VillagePeople,UnityContainerDemo"/>
6       </container>
7     </containers>
8   </unity>

  第一個錯誤是“UnityContainerDemo.Test02.IWaterTool,UnityContainerDemo”未被識別,第二個錯誤是“UnityContainerDemo”程序集無效,這種是使用UnityConfigurationSection方式注冊的,使用RegisterType方法注冊就沒什么問題,不知道是我節點配置有問題,還是代碼寫的有問題,如果有知道的朋友,還請賜教,為了這個問題,調試了好久,最終沒辦法把代碼都單獨放開。

Unity的app.config節點配置

  上面所說的三種注入方式,包括單例創建都是在代碼中去配置的,當然只是演示用,這種配置都會產生耦合度,比如添加一個屬性注入或是方法注入都要去屬性或是方法前加[Dependency]和[InjectionMethod]標記,我們想要的依賴注入應該是去配置文件中配置,當系統發生變化,我們不應去修改代碼,而是在配置文件中修改,這才是真正使用依賴注入解決耦合度所達到的效果,先看下Unity完整的配置節點:

  上面的圖大家可能都見過,我再貼一下Unity示例節點配置,原文地址:msdn.microsoft.com/en-us/library/ff647848.aspx,稍微翻譯了下。

 1 <?xml version="1.0"?>
 2 <configuration>
 3   <configSections>
 4     <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
 5              Microsoft.Practices.Unity.Configuration" />
 6   </configSections>
 7   <typeAliases>
 8     <!--壽命管理器類型-->
 9     <typeAlias alias="singleton" type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager,Microsoft.Practices.Unity" />
10     <typeAlias alias="external" type="Microsoft.Practices.Unity.ExternallyControlledLifetimeManager, Microsoft.Practices.Unity" />
11     <!--用戶定義的類型別名-->
12     <typeAlias alias="IMyInterface" type="MyApplication.MyTypes.MyInterface, MyApplication.MyTypes" />
13     <typeAlias alias="MyRealObject" type="MyApplication.MyTypes.MyRealObject, MyApplication.MyTypes" />
14     <typeAlias alias="IMyService" type="MyApplication.MyTypes.MyService, MyApplication.MyTypes" />
15     <typeAlias alias="MyDataService" type="MyApplication.MyTypes.MyDataService, MyApplication.MyTypes" />
16     <typeAlias alias="MyCustomLifetime" type="MyApplication.MyLifetimeManager, MyApplication.MyTypes" />
17   </typeAliases>
18   <unity>
19   <containers>
20     <container name="containerOne">
21       <types>
22         <!--類型映射無一生-默認為“瞬時”-->
23         <type type="Custom.MyBaseClass" mapTo="Custom.MyConcreteClass" />
24         <!--使用上面定義的別名類型的映射-->
25         <type type="IMyInterface" mapTo="MyRealObject" name="MyMapping" />
26         <!--使用類型別名指定的終身-->
27         <type type="Custom.MyBaseClass" mapTo="Custom.MyConcreteClass">
28           <lifetime type="singleton" />
29         </type>
30         <type type="IMyInterface" mapTo="MyRealObject" name="RealObject">
31           <lifetime type="external" />
32         </type>
33         <!--使用完整的類型名指定終身經理-->
34         <!--的一生經理指定的任何初始化數據-->
35         <!--將要使用的默認類型轉換器轉換-->
36         <type type="Custom.MyBaseClass" mapTo="Custom.MyConcreteClass">
37           <lifetime value="sessionKey" type="MyApplication.MyTypes.MyLifetimeManager,MyApplication.MyTypes" />
38         </type>
39         <!--使用一個自定義TypeConverter的終身管理器初始化-->
40         <type type="IMyInterface" mapTo="MyRealObject" name="CustomSession">
41           <lifetime type="MyCustomLifetime" value="ReverseKey" typeConverter="MyApplication.MyTypes.MyTypeConverter,MyApplication.MyTypes" />
42         </type>
43         <!--對象在配置中定義的注入參數-->
44         <!--使用上面定義的別名類型的映射-->
45         <type type="IMyService" mapTo="MyDataService" name="DataService">
46           <typeConfig extensionType="Microsoft.Practices.Unity.Configuration.TypeInjectionElement, Microsoft.Practices.Unity.Configuration">
47             <constructor>
48               <param name="connectionString" parameterType="string">
49                 <value value="AdventureWorks"/>
50               </param>
51               <param name="logger" parameterType="ILogger">
52                 <dependency />
53               </param>
54             </constructor>
55             <property name="Logger" propertyType="ILogger" />
56             <method name="Initialize">
57               <param name="connectionString" parameterType="string">
58                 <value value="contoso"/>
59               </param>
60               <param name="dataService" parameterType="IMyService">
61                 <dependency />
62               </param>
63             </method>
64           </typeConfig>
65         </type>
66       </types>
67 
68       <instances>
69         <add name="MyInstance1" type="System.String" value="Some value" />
70         <add name="MyInstance2" type="System.DateTime" value="2008-02-05T17:50:00" />
71       </instances>
72 
73       <extensions>
74         <add type="MyApp.MyExtensions.SpecialOne" />
75       </extensions>
76 
77       <extensionConfig>
78         <add name="MyExtensionConfigHandler" type="MyApp.MyExtensions.SpecialOne.ConfigHandler" />
79       </extensionConfig>
80     </container>
81   </containers>
82   </unity>
83   <startup>
84     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
85   </startup>
86 </configuration>
View Code

  配置過unity的朋友看一下可能就清楚,這邊我們再簡單說下:

  • Unity的配置節的名稱為”Unity",節處理程序的類型為 Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,它包含在程序集Microsoft.Practices.Unity.Configuration 中,當前程序添加該程序集的引用。
  • typeAliases管理生命周期類型,以及一些類型別名的設置,方便我們映射對象的編寫,比如同一個類型注冊多次,我們只要在typeAlias添加一個類型別名,這樣我們再添加這個類型映射的時候只要寫個別名就可以了。
  • containers是容器container集合,我們可以配置多個容器類型,通過Name屬性就可以訪問,比如訪問defaultContainer容器代碼:configuration.Configure(container, "defaultContainer");
  • container為容器管理,下面包含多個類型映射,我們平常使用的構造器注冊、屬性注冊和方法注冊,就可以在constructor、property、method節點進行配置。

  根據上面的配置,我做了一個簡單的方法注入示例,配置注冊信息都是在文件中,并不是使用RegisterType方法進行注冊,但是報下面錯誤:

  無法識別的元素“typeConfig”。示例是完全按照unity配置說明進行配置的,Google或是百度都找不到問題所在,也試了很多種方式,最后Google英文“Unrecognized element 'typeConfig'”,終于找到問題:http://unity.codeplex.com/discussions/209002,英文我大概看得不是很懂,好像是unity版本問題,unity2.0配置文件中并沒有typeConfig節點,而我們使用的unity程序集是最新的,而并沒有識別typeConfig節點,這個問題很嚴重,也花了我兩天時間,就像那位提問者最后所說:“I can not tell you how grateful I am, I've spent the last 2 days trying to figure this one out.”,雖然問題原因找到了,但是是通過英文搜索找到了,就是說國外遇到過類似問題,難道我們國內沒有遇到過,郁悶。

  大家可能有些納悶,為什么你上面幾個示例使用的unity配置沒有報錯,我當時在寫示例的時候只是使用簡單的類型映射,并不是使用完整的unity配置文件,然后就網上找了下,就找到了msdn.microsoft.com/en-us/library/ff647848.aspx,當時沒有注意版本問題,所以就出現了上面的問題,我們上面示例unity配置文件中Register節點就是unity2.0的配置,這也是為什么上面示例可以運行的原因。

  unity2.0的配置文件配置說明找了好久,終于在MSDN找到了:http://msdn.microsoft.com/en-us/library/ff660914%28v%3Dpandp.20%29.aspx#config_constructor,沒有中文版本,英文不好的朋友可以使用Google簡單翻譯下,注意這段話“Unity 2.0 uses a new streamlined configuration schema for configuring Unity.”,這才是我們要使用的unity2.0的配置文件,其實和1.2版本差不多,只不過是簡化了一些東西,配置起來也更加方便,這邊就不多說了,列一下配置目錄:

  在unity1.2中我們使用構造器注入、屬性注入和方法注入會有parameterType節點,就是說在constructor、property和method這些節點可以配置這些方式注入所依賴的類型,但是在unity2.0并不存在parameterType節點了,所有類型注冊都是通過register節點進行配置的,相當于unity1.2中的type節點,雖然unity2.0存在constructor、property和method節點,但我感覺只是針對構造器、屬性和方法本身進行注入。

  另外在unity2.0配置中alias節點下的生命管理周期配置并不需要了,比如我們創建一個單例注冊類型,只需要配置下面就可以了:

1   <containers>
2     <container name="defaultContainer">
3       <register type="UnityContainerDemo.IPeople, UnityContainerDemo" mapTo="UnityContainerDemo.VillagePeople01, UnityContainerDemo">
4         <lifetime type="singleton" />
5       </register>
6       <register type="UnityContainerDemo.IWaterTool, UnityContainerDemo" mapTo="UnityContainerDemo.PressWater, UnityContainerDemo"/>
7     </container>
8   </containers>
9   </unity>

  并不需要再像unity1.2中創建下面節點:

1 <typeAlias alias="singleton" type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager,Microsoft.Practices.Unity" />

后記

  本篇中的代碼稍微整理了下,有興趣的朋友可以下載看看,地址:http://pan.baidu.com/s/1pJAtdoR

  關于Unity依賴注入其實還有很多的東西,看一下MSDN目錄就知道了:

  但都沒有中文版本,本文只是拋磚引玉,也希望園友們可以多寫一些關于Unity依賴注入的東西,也是擴充Unity的中文資料。

  另外插一句,我在寫博客的時候喜歡在草稿箱瀏覽很多次,不管是內容上或是排版字體上,寫一段,瀏覽一段,修改一段,有點小強迫癥哈,但是是對自己負責,也是對別人負責。

  如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^

 


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()