iT邦幫忙

2022 iThome 鐵人賽

DAY 27
0
Software Development

程式基礎概念討論系列 第 27

[DAY 27] 突破作用域限制的靜態成員

  • 分享至 

  • xImage
  •  

大家可能會發現,當我們以 C# 為例子的時候,主函式跟它的類別中的其他函式或全域變數都會被加上 static 修飾詞。而今天,我們便要討論的便是這些被加上 static 修飾詞的全域變數或函式,它們被稱為靜態成員 (Static Member)

從之前介紹類別與物件的部分中,我們了解了在一般的情況下我們需要建立新的物件來使用類別中宣告與定義的變數與函式。不過,還是會有例外的狀況,例如主函式所在的類別,有些類別的設立目的並不是為了作為獨立的物件而使用,而是用來存取一些公用的變數與函數。我們曾經提到,作用域限制了變數的存取範圍,但當我們擴大範圍來看時,我們會發現作用域的限制在類別與類別之間也適用,可是這時候即便是使用了全域變數,我們也不可以跨類別去使用該變數。因此,我們便需要一個方式來準備可以跨類別使用的變數。另一方面,函式也會受到作用域的限制,因此,為了配合可以跨類別使用的變數,我們也需要可以跨類別使用的函式。為了表示它們跟一般情況下需要在程式執行時建立新的物件來使用的變數跟函式,我們把這些可以跨類別使用的變數跟函式稱為靜態成員。

靜態成員的特點有以下幾項:

  1. 在程式開始時便會自動建立並儲存在記憶體中
    無論在程式中有沒有被使用或呼叫,只要它被宣告為靜態成員,在程式一開始的時侯,便會自動建立並儲存在記憶體中,而且不會在程式執行的中途把該成員刪除或釋放記憶體。相對地,非靜態的函式或建立物件所使用的記憶體則會在函式完結時及物件被刪除時重新釋放。因此,如果過度的使用靜態成員的話,會不必要地佔用很多記憶體。

  2. 不需要使用 new 關鍵字來建立新的物件
    由於靜態成員在程式一開始的時侯便會自動建立並儲存在記憶體中,因此我們在使用前不用也不能建立物件來使用它。只有非靜態成員才需要我們建立類別的實體 (Instance),也就是物件,來讓它們實際存在在程式及記憶體中讓我們使用。

  3. 對於類別建立的物件來說,靜態成員是共用的
    即使為類別建立了多個物件,雖然一般的變數跟函式是互相獨立的,但靜態成員卻是處於共用的狀態。因此在物件 A 修改了靜態變數 A 後,在物件 B 中查看的靜態變數 A 也會是修改後的狀態。[C#]

using System;

public class Counter {
    public static int staticCounter = 0; // 靜態變數
    public int counter = 0; // 非靜態變數
    public Counter() {
        staticCounter += 1; // 靜態變數的值 += 1
        counter += 1; // 非靜態變數的值 += 1
    }
    public void checkCounters() {
        Console.WriteLine("靜態變數 = " + staticCounter);
        Console.WriteLine("非靜態變數 = " + counter);
    }
}

public class StaticExample
{
    public static void Main(string[] args)
    {
        Counter counterA = new Counter();
        counterA.checkCounters(); // 顯示: 靜態變數 = 1, 非靜態變數 = 1
        Counter counterB = new Counter();
        counterB.checkCounters(); // 顯示: 靜態變數 = 2, 非靜態變數 = 1 <-- 我們可以看到在 counterB 建立時靜態變數已經是 1,這是因為靜態變數是共用的,因此建立 counterA 時對靜態變數加 1 的行為被保留至 counterB 中,而非靜態變數則沒有影響
        counterA.checkCounters(); // 顯示: 靜態變數 = 2, 非靜態變數 = 1 <-- 同樣地,在 counterB 中對靜態變數的改動也能在 counterA 中看到
    }
}
  1. 靜態函式中沒辦法直接使用非靜態成員
    在非靜態函式中,我們可以自由地使用靜態跟非靜態的變數與函式。可是,在靜態函式中,我們只能直接使用靜態的變數與函式,如果需要使用非靜態的變數與函式,建立自身類別的物件後才可以使用。這就是為什麼通常主函式跟它的類別中的其他函式或全域變數都會作為靜態成員使用。[C#]
using System;

public class StaticExample
{
    public int num = 10; // 非靜態變數
    public static int staticNum = 20; // 靜態變數
    
    public void showNum() { // 非靜態函式
        Console.WriteLine(num); // 可以直接使用非靜態變數
        Console.WriteLine(staticNum); // 可以直接使用靜態變數
        staticFunc(); // 可以直接使用靜態函式
    }
    
    public static void staticFunc() { // 靜態函式
        Console.WriteLine("Static Function");
    }
    
    public static void Main(string[] args) // 靜態函式
    {
        Console.WriteLine(staticNum); // 可以直接使用靜態變數
        staticFunc(); // 可以直接使用靜態函式
        Console.WriteLine(num); // 錯誤! 無法直接使用非靜態變數
        showNum(); // 錯誤! 無法直接使用非靜態函式
        StaticExample obj = new StaticExample(); // 需要建立自身類別的物件來使用非靜態成員
        Console.WriteLine(obj.num); // 透過作為實體 (instance) 的物件便可以使用非靜態變數
        obj.showNum(); // 透過作為實體 (instance) 的物件便可以使用非靜態變數
    }
}
  1. 無法被繼承到子類別
    靜態成員並不屬於程式執行中建立的物件中的成員,因此並不適用物件導向的特性,我們無法透過繼承讓子類別獲得該變數或函式,也不能進行覆寫。不過,封裝的原則仍然可以用於靜態成員,不過進行封裝時較建議使用靜態成員,否則會在建立新物件時重複建立修改及存取的相關變數及函式。[C#]
using System;

public class StaticGetterSetter {
    private static int _num = 0; // 把靜態變數隱藏起來,使類別外無法直接存取它
    // 這裡使用了非靜態變數來進行修改及存取靜態變數的函式,但理想狀態下應同樣使用靜態變數 public static int num { ... }
    public int num {
        get { // 簡易版存取函式
            return _num;
        }
        set { // 簡易版修改函式
            _num = value;
        }
    }
    ...
}

public class StaticExample
{
    public static void Main(string[] args)
    { 
        // 這裡我們建立了 3 個物件,因此非靜態變數 num 也被重複建立了 3 次,由於 num 變數是用來存取靜態變數,我們可以發現這裡建立 3 次變數是非常多餘且浪費記憶體的行為
        // 因此,如果我們使用靜態變數 public static int num { ... } 的話,便能避免浪費記憶體
        StaticGetterSetter test1 = new StaticGetterSetter();
        StaticGetterSetter test2 = new StaticGetterSetter();
        StaticGetterSetter test3 = new StaticGetterSetter();
    }
}

在類別外使用靜態成員

靜態成員作為可以跨類別使用的變數跟函式,自然是可以在其他類別中使用。由於沒有建立物件,因此使用的方式大多是以類別名稱來輔助存取及呼叫。

非靜態成員:
(物件名稱).(變數/函式名稱)
靜態成員:
(類別名稱).(變數/函式名稱)

以下是一個簡單的例子:[Python]

class StaticExample:
    def non_static_function(self):
        print("Non Static Function")
    @staticmethod # 宣告靜態函式
    def static_function():
        print("Static Function")

StaticExample.static_function() # 使用類別呼叫靜態函式
example = StaticExample() # 建立物件
example.non_static_function() # 使用物件呼叫非靜態函式

靜態類別

靜態類別是被宣告為靜態的類別,靜態類別中只能存在靜態成員,並且無法被繼承或建立物件,可以用來避免只存放靜態成員的類別在程式中不小心建立物件而浪費記憶體的行為。不過,不是所有的程式語言都支援靜態類別,因此使用靜態類別前建立大家先查詢該語言是否支援再使用。


上一篇
[DAY 26] 在函式中傳遞資料時要注意的細節
下一篇
[DAY 28] 不怕一萬只怕萬一的例外處理
系列文
程式基礎概念討論30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言