Course Review - C# Intermediate Classes, Interfaces and OOP
2020-06-25
五月的時候趁著 300 元特價的時候一口氣買了十門課程,這段期間各課程都散亂的看了一點,尚未完成任何一門課程。而上週開始一點一滴的看,默默就把這門課程看完了,過程中刷新了自己對於物件導向的觀念、各種修飾詞的實務使用時機以及開發操作上的技巧,總體而言獲益良多,也助燃提升學習 C# 的興趣。
課程的收穫
Field & Property (Encapsulation)
曾經的我並不是那麼清楚兩者的差別,現在終於知道是為了基於物件的封裝(Encapsulation),才有 Field 與 Property 的差別。Field 作為物件本身的內部資訊,提供方法的行為邏輯使用,並不直接面對外界;Property 則是外界需要存取的關於物件的公開資訊。兩者容易令人混淆的原因就是往往 Property 看似就只是語法差異的 Field,例如:
public string Name; // Field
public string Name { get; set; } // Property
對於物件實例 (instance) 而言兩者存取 (get) 或者寫入 (set) 值的方式並沒有差異,然而實際上最大的差異就是封裝的精神。Public field 是將內部資訊暴露於外部,property 則隱含了一個 private field ,property 只是此 private field 與外界互動的中介:
private string name; // Private Field
public string Name { get => _name; set => _name = value; } // Property
而在 Visual Studio IDE 的提示下,會鼓勵使用 AUTO Property 於是最後就成為了:
public string Name { get; set; } // Property
試作一個小範例,遵循著封裝的精神,並且依照 IDE 的提示使用 AUTO Property。類別 (Class) 有 Weight Property,可以提供外界存取或者寫入,同時也提供了 kg, lb, oz 等單位轉換,但不是透過 Method ,因為 Method 是關於物件的行為,Private Field 的轉換就讓 Property 來完成。
public class CellPhone
{
public int Weight { get; set; }
public double WeightKg { get => Weight * 0.001; }
public double WeightLb { get => Weight * 0.0022; }
public double WeightOz { get => Weight * 0.03527; }
}
class Program
{
static void Main(string[] args)
{
var cellphone = new CellPhone();
cellphone.Weight = 300;
Console.WriteLine($"grams : {cellphone.Weight}");
Console.WriteLine("-----Convert-----");
Console.WriteLine($"Kg : {cellphone.WeightKg}");
Console.WriteLine($"Lb : {cellphone.WeightLb} \nOz : {cellphone.WeightOz}");
}
}
Object Initializers over Constructor
儘管 Constructor 提供了多載 (Overloading) 的方式,可以用不同的函式簽名 (Signature),但如果有多個 Field 為此建造多個 Constructor 會讓程式碼變得凌亂。因此課程建議除非是有物件的建構有需要複雜的建構行為,否則建議使用 Object Initializers 來替代多數的 Constructor。因為 Object Initializers 可以為 Public Field 及 Property 設定值,甚至是其他型別物件參與也可以:
⚠️示範多個建構子,程式碼凌亂
public class Person
{
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public Person(string name)
{
this.Name = name;
}
public Person(DateTime birthdate)
{
this.BirthDate = birthdate;
}
public Person(string name, DateTime birthdate)
{
this.Name = name;
this.BirthDate = birthdate;
}
public int Age { get => (DateTime.Now - BirthDate).Days / 365; }
}
✔️使用 Object Initializers
class Program
{
static void Main(string[] args)
{
var webber = new Person { Name = "Webber", BirthDate = new DateTime(2002, 6, 25) };
Console.WriteLine($"{webber.Name}'s age : {webber.Age}");
}
}
public class Person
{
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public float Age { get => (DateTime.Now - BirthDate).Days / 365; }
}
Params Modifier
讓 Method 可以支援不定數量的 arguments。
public int Count(params int[] numbers)
{
return numbers.Sum();
}
object.Count(10, 20, 30);
object.Count(10, 20, 30, 40, 50);
Indexer
public class HttpCookie
{
private readonly Dictionary<string, string> _dictionary;
public HttpCookie()
{
_dictionary = new Dictionary<string, string>();
}
public string this[string key]
{
get { return _dictionary[key]; }
set { _dictionary[key] = value; }
}
}
var cookie = new HttpCookie();
cookie["over18"] = "1";
Console.WriteLine(cookie["over18"]);
Favour Composition over Inheritance
Inheritance 的缺點:
- 容易被濫用,造成日後維護上的不便(牽一髮動全身)
- 高耦合
Composition 的優點:
- Reuse Code as Inheritance
- 低耦合
// Inheritance
public Car : Vehicle
{
}
// Compisitoin
public Car
{
public system EngineSystem {get; set;}
public system DriveSystem {get; set;}
}
Constructors and Inheritance
- 建構子不會被子類別繼承,必須重新為子類別定義建構子
- 當建立物件實體時,會先執行父類別的建構子、再依序執行至子類別的建構子
- 可以使用 base 來指定執行父類別的建構子
public class Vehicle
{
public Vehicle()
{
Console.WriteLine("vehicle is constructed");
}
public Vehicle(string materialName)
{
Console.WriteLine($"vehicle is constructed with material : {materialName}");
}
}
public class Car : Vehicle
{
public Car(string name) : base("ZMC")
{
Console.WriteLine($"car is constructed : {name}");
}
}
var car = new Car("Neo-TriDagger");
// vehicle is constructed with material : ZMC
// car is constructed :
Upcasting & Downcasting
- Upcasting : 將子類別轉型為父類別,會失去子類別專有的屬性與方法,與多型(Polymorphism) 結合使用
- Downcasting : 將父類別轉型為子類別,取得子類別專有的屬性與方法
Vehicle vehicle = new Car(); // Upcasting
Car car = (Car)vehicle; // Donwcasting
Boxing and Unboxing
- C# types 分為 value types(int, c) 與 reference types.
- Value types (如 int, char, bool, all primitive types and struct) are stored in the stack.
- Reference types (如類別) are stored in the heap.
- 因為 object class 是所有類別的父類別 (parent of all classes) 所以 value type instance 可以被隱含地 (implicitly) 轉換為 object reference。
- Boxing 是將 value type instance 轉換為 object reference.
- Unboxing 則相反,將 object reference 轉換為 value type.
- Boxing & Unboxing 轉換上有效能的成本,當轉換的數量大的時候影響會非常顯著
Where Should I Use It
例如 ArrayList collection 僅接受物件 (reference type) 當作其元素,因此必須藉由 boxing 來將 value type 存入其中。
Virtual & Override Modifier
用以表示父類別中的方法可以被子類別覆寫 (Override),藉由 Method Override 可以實踐 Polymorphism ,避免 procedural switch statements or conditionals,並且讓類別保持 Open Closed Principle (對於需求保持開放實作新程式碼的彈性;對於修改既有程式碼保持封閉的避免)。
⚠️Override 的函式必須要與父函式有相同的簽名。
public class Shape
{
public virtual double GetArea()
{
throw new NotImplementedException();
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double GetArea()
{
return Radius * Radius * Math.PI;
}
}
public class Triangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double GetArea()
{
return Width * Height * 0.5;
}
}
var shapes = new List<Shape>
{
new Circle { Radius = 3 },
new Triangle { Width = 5, Height = 3 }
};
foreach (var shape in shapes)
{
Console.WriteLine(shape.GetArea());
}
MSDN - Virtual
MSDN - Override
Sealed Modifier
可以令屬性或者方法無法被衍生類別覆寫。一般而言父類別沒有加上 virtual 修飾就無法被衍生類別給覆寫,但覆寫除了受 virtual 所允許外,父類別覆寫自祖父類別的 virtual 或者實作祖父類別的 abstract 都可以被子類別覆寫,因此使用的時機便是如果要覆寫結束在父類別,便可以在父類別的實作上加上 sealed,避免子類別再次覆寫或者實作。
class X
{
protected virtual void F() { Console.WriteLine("X.F"); }
protected virtual void F2() { Console.WriteLine("X.F2"); }
}
class Y : X
{
sealed protected override void F() { Console.WriteLine("Y.F"); }
protected override void F2() { Console.WriteLine("Y.F2"); }
}
class Z : Y
{
// Attempting to override F causes compiler error CS0239.
// protected override void F() { Console.WriteLine("Z.F"); }
// Overriding F2 is allowed.
protected override void F2() { Console.WriteLine("Z.F2"); }
}
Interface
- Interface 作為一種宣告,其成員中的方法在宣告時不實作 (implementation),而其所宣告成員包括方法 (methods) 及 屬性 (properties), 但不包括 fields (因為 fields 是關於細節的實作)
- Interface 的屬性及方法成員沒有存取修飾詞
- Interfaces 是建造低耦合應用程式的利器,藉由 interfaces 如果互相關係的類別之一改變了,只要介面仍保持一致,對於依賴 (dependent) 它的類別能不受影響
public interface ITaxCalculator
{
int Calculate();
}
public class TaxCalculator : ITaxCalculator
{
public void Calculate() { … }
}
public interface IPizzaIngredientFactory
{
Dough CreateDough();
Sauce CreateSauce();
Cheese CreateCheese();
List<Veggies> CreateVeggies();
Pepperoni CreatePepperoni();
Clams CreateClam();
}
Testability
為了達成單元測試,類別之間不能有依賴,藉由 interface 可以處理類別之間依賴的問題。
Extensibility
藉由實作介面可以擴充程式碼(新類別),而不需要修改既有的程式碼。對於依賴介面的既有類別,新實作的類別可以透過實作介面,成為既有類別在執行期間的新演算邏輯,而不需要異動既有的程式碼結構。
Inheritance
Interface 不是為了解決多重繼承而出現的,interface 提供了程式碼的方法的宣告,但實作必須由類別所完成,因此不是繼承的關係。
三個練習題,重構教我們的事
StopWatch
- 釐清 property 是否有必要?注意物件的封裝是否使用 private field 即可以滿足演算邏輯,避免資訊的過度暴露造成可能造成使用上的非預期的問題
- if 先處理例外狀況,正常流程則放在 else
if (booleanCondition)
{
throw new Exception();
}
else
{
...
}
Stack
- 注意程式碼是否有冗餘的部分(沒有也不影響演算邏輯),移除它。
- 注意命名的習慣 (conventions)
Workflow Engine
- 回傳的型別可以藉由 Interface 來縮限外界能夠操作的範圍,並確保外界使用公開的函式以符合預期。例如回傳 IEnumerable
型別,保護 IList ,讓外界無法直接使用 Add, Remove 等方法,必須透過公開的操作函式來進行操作。
物件導向的操作
多數的時候透過按下 Alt + Enter
讓 IDE 來提示最佳化的物件導向重構。
- private field 命名慣例 _variable
- private field 如果是 reference instance 加上 readonly
- 將 Property <=> Auto Property <=> Field & Method 等型式之間做轉換
- 抽出 Class 為 Class.cs 獨立檔案