字數總計:0 個 | 閱讀時長:0 分鐘 |閱讀次數:

前言

任何軟體架構的實作都離不開具體的程式碼,所以我們對軟體架構的討論應該從第一行被寫下的程式碼開始。

  • 1938 年,Alan Turing 為現代電腦程式設計打下了基礎。他也是第一個提出「程式即數據」的人。
  • 1945 年,Turing 已經在真實電腦上編寫我們現在也能看懂的電腦程式了。這些程式中用到了循環、分支、賦值、子呼叫、堆疊等如今我們都很熟悉的結構。而他用的程式語言就是簡單的二進制數序列。
  • 20 世紀 40 年代末期出現了彙編器(assembler),它能自動將一段程式轉換為相應的二進制數序列,大幅解放了程式設計師。
  • 1951 年,Grace Hopper 發明了 A0,這是世界上第一個編譯器(compiler)。事實上,編譯器這個名字就是他定義和推廣使用的。
  • 1953 年,那一年 FORTRAN 面世了。接下來就是層出不窮的新程式語言了 —— COBOL、PL/1、SNOBOL、C、Pascal、C++、Java 等等,不勝枚舉。

除此之外,電腦程式設計領域還經歷了另一個更巨大、更重要的變革,那就果程式設計範式(paradigm)的變化。程式設計範式指的是程式的編寫模式,與具體的程式語言關係相對較小。這些範式會告訴你應該在什麼時候採用什麼樣的程式碼結構。直到今天,我們也總共只有三個程式設計範式,而且未來幾乎不可能再出現新的,接下來我們就來看看為什麼。

結構化程式設計

結構化程式設計(Structured programming,簡稱SP)是一種程式設計的範式,旨在透過使用子程序區塊結構迴圈決策控制結構(如 if-else 語句和 switch 語句)來增强程式碼的清晰度、品質和開發效率。它鼓勵程序員避免使用 goto 語句,這樣可以使程式碼更易於理解和維護。結構化程式設計強調的是將大問題分解成小問題,並將這些小問題分解成更小的問題,直到這些問題可以用簡單的語句來解決。

使用 goto 及結構化的程式碼比較

我們現在有一個任務是從 1 數到 10,但當數到 5 時,我們要跳過 5 直接數到 6,繼續數到 10。

使用 goto 的程式碼:

using System;

class GotoComplexExample {
    static void Main() {
        int num = 1;
        
        start:
        if (num == 5) {
            goto skipFive;
        }
        
        Console.WriteLine(num);
        num++;
        
        if (num <= 10) {
            goto start;
        }
        goto end;
        
        skipFive:
        num++;
        goto start;
        
        end:
        Console.WriteLine("Done");
    }
}

結構化的程式碼:

using System;

class StructuredComplexExample {
    static void Main() {
        for (int num = 1; num <= 10; num++) {
            if (num == 5) {
                continue; // 跳過5,直接繼續下一輪循環
            }
            Console.WriteLine(num);
        }
        Console.WriteLine("Done");
    }
}

以下是結構化程式設計的主要概念和重點:

  1. 基本控制結構:
    • 順序結構:程式碼按順序執行。
      程式碼演示
      Console.WriteLine("第一步");
      Console.WriteLine("第二步");
      Console.WriteLine("第三步");
      

    • 選擇結構:根據條件選擇不同的執行路徑,如if-else語句。
      程式碼演示
      var number = 10;
      if (number > 0)
          Console.WriteLine("數字是正數。");
      else if (number < 0)
          Console.WriteLine("數字是負數。");
      else
          Console.WriteLine("數字是零。");
      

    • 迴圈結構:重複執行程式碼塊直到滿足某個條件,包括for迴圈、while迴圈等。
      程式碼演示
      for (int i = 0; i < 5; i++)
          Console.WriteLine($"迴圈次數:{i}");
      

  2. 模塊化和函數:
    • 將大的問題分解成小的、更易於管理的子問題。
    • 每個子問題由一個模塊或函數來解決,使程式碼更易於理解和重用。
    程式碼演示
    // 定義一個函數來計算兩數之和
    static int Add(int num1, int num2) {
        return num1 + num2;
    }
    
    // 主程式調用Add函數
    static void Main(string[] args) {
        int result = Add(5, 3);
        Console.WriteLine($"5 + 3 = {result}");
    }
    

  3. 局部變量和作用域:
    • 推廣在最接近使用位置聲明變量,以提高程式碼的可讀性和減少錯誤。
    • 使用局部變量來限制變量的作用範圍,增強程式的模塊性。
    程式碼演示
    static void ShowScopeExample() {
        // 局部變量i的作用域開始
        int i = 100;
        Console.WriteLine($"在ShowScopeExample內部,i = {i}");
        // 局部變量i的作用域結束
    }
    
    static void Main(string[] args) {
        ShowScopeExample();
        // 以下將無法訪問i,因為它的作用域僅限於ShowScopeExample函數內
        // Console.WriteLine(i); // 這會導致編譯錯誤
    }
    

  4. 結構化設計的原則:
    • 自頂向下設計:從最廣泛的問題開始,逐步細化到具體的功能和細節。
    • 逐步細化:將複雜的問題逐步分解成更簡單、更易於管理的部分。
    • 模塊獨立性:提高模塊之間的獨立性,降低它們之間的耦合。

使用結構化程式設計的優點:

  • 提高程式碼的清晰度和可維護性。
  • 促進程式碼重用。
  • 減少錯誤和開發時間。
  • 使程式設計更加系統化和規範化。

結構化程式設計是對早期無結構、難以維護的程式設計方法的一種改進,它強調清晰的結構和簡單的控制流。隨著軟件工程的發展,雖然出現了許多新的程式設計範式(如物件導向程式設計),結構化程式設計仍然是教授初學者基本程式設計技巧和實踐的重要方法。

物件導向程式設計

物件導向程式設計(Object-Oriented Programming,簡稱OOP)是一種程式設計範式,它使用「物件」(objects)這個概念來模擬現實世界中的事物和事物間的互動,從而實現軟件的封裝繼承多型性等特性。以下是物件導向程式設計的主要概念和重點:

  1. 類別(Class)
    • 定義了一組具有相同屬性(Attributes)和行為(Methods)的物件的藍圖或模板。
    • 類別是創建物件的基礎,物件是類別的實例。
  2. 物件(Object)
    • 是類別的實例,具有類別定義的屬性和行為。
    • 物件是程式運行時操作的基本單位。
  3. 屬性(Attributes)
    • 代表物件的狀態或特徵,例如:一輛車的顏色、型號等。
    • 在類別中定義,每個物件可以有自己的屬性值。
  4. 方法(Methods)
    • 描述物件能夠執行的操作,例如:一輛車可以加速、停車等。
    • 方法在類別中定義,物件通過方法與外界互動。
  5. 封裝(Encapsulation)
    • 指將物件的細節(屬性和方法)隱藏起來,只暴露有限的介面給外部使用。
    • 通過封裝,可以減少外部對物件內部的依賴,提高程式的模塊性和可維護性。
  6. 繼承(Inheritance)
    • 允許新的類別繼承現有類別的屬性和方法。
    • 繼承支持代碼重用,並能建立一個層次化的類別結構。
  7. 多型性(Polymorphism)
    • 允許不同類別的物件對同一訊息做出回應。
    • 通過多型性,相同的接口可以用於不同的物件,增強程式的彈性和可擴展性。
  8. 抽象(Abstraction)
    • 是將複雜的實際問題抽象化,只關注對當前目標有用的資訊。
    • 通過抽象,可以隱藏背後的複雜實現,只提供必要的操作。
程式碼演示
  1. 定義抽象基類 - Vehicle:在這個步驟中,Vehicle將被定義為一個抽象類別,它包含了一個抽象方法DisplayInfo。
    public abstract class Vehicle
    {
        // 屬性
        public string Brand { get; set; }
        public int Year { get; set; }
    
        // 建構子
        public Vehicle(string brand, int year)
        {
            Brand = brand;
            Year = year;
        }
    
        // 抽象方法
        public abstract void DisplayInfo();
    }
    
  2. 定義子類別 - Car 和 Truck:這裡的Car和Truck類別將實現(override)基類的DisplayInfo抽象方法。
    public class Car : Vehicle
    {
        public int Seats { get; set; }
    
        public Car(string brand, int year, int seats) : base(brand, year)
        {
            Seats = seats;
        }
    
        // 實現抽象方法
        public override void DisplayInfo()
        {
            Console.WriteLine($"Car: {Brand}, Year: {Year}, Seats: {Seats}");
        }
    }
    
    public class Truck : Vehicle
    {
        public int LoadCapacity { get; set; }
    
        public Truck(string brand, int year, int loadCapacity) : base(brand, year)
        {
            LoadCapacity = loadCapacity;
        }
    
        // 實現抽象方法
        public override void DisplayInfo()
        {
            Console.WriteLine($"Truck: {Brand}, Year: {Year}, Load Capacity: {LoadCapacity}kg");
        }
    }
    
  3. 展示封裝、繼承、多型性和抽象的應用
    class Program
    {
        static void Main(string[] args)
        {
            // 創建物件
            Vehicle myCar = new Car("Toyota", 2020, 5);
            Vehicle myTruck = new Truck("Ford", 2018, 1200);
    
            // 通過多型性處理不同的物件
            List<Vehicle> vehicles = new List<Vehicle> { myCar, myTruck };
    
            foreach (Vehicle vehicle in vehicles)
            {
                vehicle.DisplayInfo();
            }
        }
    }
    

在這個範例中,Vehicle是一個抽象基類,它定義了一個抽象方法DisplayInfo。Car和Truck是具體的子類別,它們實現了Vehicle中的抽象方法。這樣的設計不僅展示了繼承和多型性(因為Car和Truck都是Vehicle的類型),也利用了抽象來強制要求任何Vehicle子類別都必須提供自己的DisplayInfo方法實現。此外,屬性的使用(如Brand和Year等)以及方法的實現,展示了封裝的概念,因為它們將資料和功能封裝在各自的類別內部。透過這些概念的應用,程式碼變得更加模組化、易於維護和擴展。


物件導向程式設計的這些概念和原則共同作用,使得程式設計更加靈活、模塊化,並且易於維護和擴展。透過對這些概念的理解和應用,開發者可以更有效地處理軟件開發中的複雜問題。

函數式程式設計

函數式程式設計(Functional Programming,簡稱FP)是一種建立在數學函數概念上的程式設計範式。相對於指令式程式設計,它強調應用函數和避免改變狀態及可變數據。以下是函數式程式設計的一些核心概念整理:

  1. 不可變性(Immutability):是函數式程式設計中的一個重要概念,指的是一旦數據結構被創建後,其內部的數據就不能被改變。這意味著如果你需要修改某個數據結構,你應該創建一個新的數據結構來代替原有結構的某部分,而不是直接修改原有結構中的數據。這種方式有助於提高程式的可預測性,減少錯誤,並且在多線程環境中避免了競態條件(Race Conditions)。
    優點
    • 簡化了複雜度:不需要擔心數據在系統的某個部分被意外改變,因為數據是不可變的。
    • 提高了程式的安全性:由於數據不會改變,因此在多線程環境下沒有競態條件問題,這使得程式更加安全。
    • 便於進行時間旅行調試(Time Travel Debugging):由於每次數據變更都會產生新的數據結構,因此可以很容易地追蹤數據的變化歷史。
    程式碼演示
    • 使用不可變集合
      using System.Collections.Immutable;
      
      var list = ImmutableList<int>.Empty;
      list = list.Add(1);
      list = list.Add(2);
      // list 現在包含 [1, 2],過程中沒有修改任何集合,而是創建了新的集合。
      
    • 設計不可變類
      public class ImmutablePerson
      {
          public string Name { get; }
          public int Age { get; }
      
          public ImmutablePerson(string name, int age)
          {
              Name = name;
              Age = age;
          }
      }
      
      var person = new ImmutablePerson("Alice", 30);
      // person 的 Name 和 Age 一旦設定,就無法更改。
      
      在這個例子中,ImmutablePerson 的實例一旦創建,其 Name 和 Age 就不可更改,從而保證了實例的不可變性。這種方式有助於創建更加安全、簡潔、可維護的代碼。

  2. 純函數(Pure Functions):是函數式程式設計中的一個核心概念,它指的是這樣一種函數:對於相同的輸入,永遠會返回相同的輸出,且在過程中不會有任何副作用(Side Effects),例如不會修改任何外部狀態、不會有 I/O 操作等。
    優點
    • 可預測性(Predictability):由於純函數對相同的輸入總是產生相同的輸出,這使得它們非常可預測。這種可預測性使得程式的調試和理解變得更加容易。
    • 易於測試(Ease of Testing):純函數不依賴於外部狀態,也不產生副作用,因此它們更加容易進行單元測試。測試者無需擔心設置或清理外部環境,只需關注輸入和預期的輸出即可。
    • 無副作用(No Side Effects):純函數的執行不會對系統的其他部分產生不可預見的影響。這降低了程式在擴展或修改時產生錯誤的風險。
    • 可移植性/自包含性(Portability / Self-contained):純函數不依賴於程序的外部狀態,因此它們更容易在程式的不同部分間移植,或者用於其他程式中。
    • 可利用性(Reusability):由於純函數的自包含性和無副作用特性,它們具有很高的可重用性,可以作為程式中的構建塊,被多處使用而不必擔心意外影響。
    • 並行/並行處理(Concurrency/Parallelism):純函數不會改變任何外部狀態,這意味著多個純函數可以安全地並行或並行執行,這對於提高性能在多核心處理器的利用上尤為重要。
    • 參照透明性(Referential Transparency):由於純函數的結果僅依賴於其輸入,所以它們的呼叫可以被其輸出值替換而不會影響程式的行為,這種性質稱為參照透明性。這進一步提高了程式的可理解性和簡化了優化。
    • 更容易進行形式化驗證(Easier Formal Verification):純函數的性質使它們更容易適應形式化的驗證方法,這對於需要高度可靠性的應用場景(如金融和航空系統)非常重要。
    程式碼演示
    using System;
    
    class Program
    {
        // 定義一個純函數
        static int Add(int a, int b)
        {
            return a + b;
        }
    
        static void Main(string[] args)
        {
            int result = Add(5, 3);
            Console.WriteLine(result); // 輸出: 8
        }
    }
    

    在這個例子中,Add 函數只依賴於其輸入參數 a 和 b,不讀取或修改任何外部狀態(如全局變量或靜態變量),也不執行如打印到控制台或寫文件等有副作用的操作。因此,無論調用它多少次,只要輸入參數相同,它都會返回相同的結果。


  3. 頭等物件(Functions as First-class Citizens):是一個核心概念。這意味著在程式語言中,函數被視為一種可以存儲在變量中、作為參數傳遞給其他函數、從其他函數返回,甚至在運行時構造的值。這種對函數的處理方式提升了函數的靈活性,使得函數可以像其他數據類型一樣被操作和使用,從而促進了更高階的抽象和程式碼的重用。
    特點
    • 可存儲:函數可以被賦值給變量。
    • 可傳遞:函數可以作為參數傳遞給其他函數。
    • 可返回:函數可以作為其他函數的返回值。
    • 運行時構造:函數可以在程式運行時動態創建。
    程式碼演示

    C# 中會通過委託(Delegates)和表達式樹(Expression Trees)以及 Lambda 表達式支持將函數作為頭等物件。

    • 函數賦值給變量
      Func<int, int, int> add = (a, b) => a + b;
      int result = add(2, 3); // result = 5
      
    • 函數作為參數傳遞
      List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
      IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0);
      // Where 接受一個函數作為參數
      
    • 函數作為返回值
      Func<int, Func<int, int>> createAdder = x => y => x + y;
      var addTwo = createAdder(2);
      int result = addTwo(3); // result = 5
      

    這些例子展示了在 C# 中如何利用 Lambda 表達式和委託來實現函數作為頭等物件的概念。這種處理方式不僅增加了程式碼的靈活性和可重用性,而且也支持了更複雜的函數式程式設計技術,如高階函數和函數組合。


  4. 高階函數(Higher-order Functions):是函數式程式設計中的一個關鍵概念,指的是那些至少滿足下列一項條件的函數:
    • 接受一個或多個函數作為參數。
    • 返回另一個函數作為結果。

    高階函數的存在允許函數操作其他函數,提供了極大的靈活性和表達力,使得開發者可以用更加宣告性的方式來構造程序。
    優點
    • 抽象化程度高:可以將操作抽象成函數,使得程式碼更加簡潔和可讀。
    • 增強可重用性:通過將通用的操作抽象成函數,可以在不同的上下文中重用這些函數。
    • 便於組合和重構:函數可以作為構件進行組合,以構建更複雜的操作和數據流。
    程式碼演示
    • 接受函數作為參數的高階函數
      public static void ProcessList(List<int> list, Func<int, int> operation)
      {
          for (int i = 0; i < list.Count; i++)
          {
              list[i] = operation(list[i]);
          }
      }
      
      List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
      ProcessList(numbers, x => x * x); // 將列表中每個數字平方
      
    • 返回函數的高階函數
      public static Func<int, int> MultiplyBy(int factor)
      {
          return x => x * factor;
      }
      
      var triple = MultiplyBy(3);
      int result = triple(5); // result = 15
      

  5. 函數組合(Function Composition):是函數式程式設計的另一個核心概念,它涉及將兩個或更多的函數結合成一個新的函數。在這種模式下,一個函數的輸出成為另一個函數的輸入。這種方法允許開發者建立複雜的操作,透過組合簡單函數來實現,從而增加程式碼的模組化和重用性。
    優點
    • 模組化:透過將小的、專一的函數組合成一個複雜的函數,可以提高程式碼的模組化。
    • 重用性:單獨的函數可以在不同的組合中重複使用,提高了程式碼的重用性。
    • 可讀性和維護性:良好設計的函數組合可以使得程式碼更易於理解和維護。
    程式碼演示
    • 函數組合在 C# 中的應用
      // 定義兩個簡單函數
      Func<int, int> doubleIt = x => x * 2;
      Func<int, int> addOne = x => x + 1;
      
      // 函數組合
      Func<int, int> doubleThenAddOne = x => addOne(doubleIt(x));
      
      // 使用組合函數
      int result = doubleThenAddOne(3); // 結果為 7
      

      在這個例子中,doubleThenAddOne 就是一個組合了 doubleIt 和 addOne 的函數。首先對輸入執行 doubleIt 函數,然後將结果作為 addOne 函數的輸入。
    • C# 的鏈式調用
      List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
      var result = numbers.Select(doubleIt).Select(addOne);
      // result 是 IEnumerable<int>,包含 [3, 5, 7, 9, 11]
      

      在這個例子中,Select 方法被用來應用 doubleIt 和 addOne 函數到列表的每個元素上。展現了如何使用 LINQ 來進行函數組合。

  6. 遞迴(Recursion):是一種在程式設計中常見的技巧,其中一個函數直接或間接地呼叫自身。這種技術尤其在處理具有自然層次結構的數據,如文件系統的目錄結構、網頁的 DOM 結構、或是數據結構(例如樹和圖)時特別有用。
    優點
    • 模組化:透過將小的、專一的函數組合成一個複雜的函數,可以提高程式碼的模組化。
    • 重用性:單獨的函數可以在不同的組合中重複使用,提高了程式碼的重用性。
    • 可讀性和維護性:良好設計的函數組合可以使得程式碼更易於理解和維護。
    程式碼演示
    • 計算階乘
      public static int Factorial(int n)
      {
          if (n <= 1) // 基本情況
              return 1;
          else
              return n * Factorial(n - 1); // 遞歸步驟
      }
      
      int result = Factorial(5); // 計算 5!
      
      在這個例子中,Factorial 函數是一個遞歸函數,用來計算一個數的階乘。當 n 小於或等於 1 時,函數直接返回 1(這是基本情況,不需要進一步遞歸)。否則,函數透過 n * Factorial(n - 1) 進行遞歸調用自己(遞歸步驟),直到達到基本情況。

響應式程式設計

響應式程式設計(Reactive Programming)是一種面向數據流和變化傳播的程式設計範式。這意味著,當使用響應式程式設計時,可以輕鬆地表達靜態或動態的數據流,並且當數據流發生變化時,相關計算模型會自動更新。這種程式設計模式在處理具有高度交互性的應用程序中特別有用,例如實時數據處理系統、動態用戶界面和其他需要即時響應數據變化的應用場景。

在C#中,響應式程式設計主要是通過 Reactive Extensions(Rx)庫來實現的。Rx 是一個使用可觀察序列來進行異步程式設計和基於事件的程序庫,它擴展了觀察者模式以支持數據/事件序列,並提供了豐富的操作符來創建、合成、過濾、轉換、以及對這些序列進行其他操作。

核心概念:

  • 可觀察序列(Observable):在 Rx 中,Observable 是表示異步數據流的主要抽象。你可以訂閱(Subscribe)一個 Observable,並且當數據流發出一個或多個值時,訂閱者(Subscriber)就會收到通知。這類似於枚舉(IEnumerable)和迭代器(IEnumerator)模式,但針對的是異步和基於事件的數據流。
  • 訂閱者(Subscriber):訂閱者對 Observable 發出的項目進行響應。在 Rx 中,訂閱者實現了一個或多個接口方法來處理數據流發出的項目、錯誤以及完成信號。
優點
  • 非阻塞:數據流的處理是非阻塞的,這意味著你的應用程序可以更有效地使用系統資源,同時處理多個操作。
  • 異步處理:Rx 天生支持異步操作,這使得處理時間密集型或 I/O 密集型任務變得更加簡單。
  • 易於合成:通過 Rx 提供的豐富操作符,可以輕鬆地合成和轉換數據流,以實現複雜的業務邏輯。
  • 錯誤處理:在 Rx 中,錯誤被視為數據流的一部分。這意味著錯誤和正常數據一樣可以被處理和轉換,使得錯誤處理變得更加靈活和強大。
適用場景
  • 實時數據處理:如股票報價、實時分析等。
  • 用戶界面開發:可以輕鬆實現對用戶操作的響應,如按鈕點擊、滑動事件等。
  • 網路程式設計:處理HTTP請求、響應和其他異步I/O操作。
  • 事件驅動系統:任何需要處理事件流的系統,如消息隊列消費者、監控系統等。
程式碼演示
using System;
using System.Reactive.Linq;

namespace ReactiveExample
{
    class Program
    {
        static void Main(string[] args)
        {
            // 創建一個可觀察序列,從1開始,每隔一秒發出一個數值
            var observable = Observable.Interval(TimeSpan.FromSeconds(1))
                                        .Select(index => $"Value {index}");
            
            // 訂閱可觀察序列,並打印出發出的值
            var subscription = observable.Subscribe(value => Console.WriteLine(value));

            // 防止程序直接退出
            Console.WriteLine("Press any key to exit");
            Console.ReadKey();

            // 取消訂閱
            subscription.Dispose();
        }
    }
}

在這個示例中,我們創建了一個每秒發出一個值的可觀察序列,然後通過 Subscribe 方法訂閱了這個序列。當序列發出一個值時,我們的訂閱者就會接收到這個值並將其打印出來。這個過程會一直持續,直到我們通過調用 subscription.Dispose() 手動取消訂閱,或者 Observable 序列自行結束(例如,發出了完成信號或錯誤信號)。


總結