標籤:

Excel+VBA製作數獨(二):類模塊篇

上周對數獨進行了優化,在我的筆記本上隨機生成100個數獨時間在0.9s到1s之間,比之前快了30倍左右,優化的keypoint寫在了文中。

上次介紹完基本方法,這次來說明類模塊的內容。首先,為什麼要用類模塊?這個問題其實就是在問為什麼要用面向對象編程。在VBA中我個人其實不太喜歡用類模塊,一是沒有其他語言的面向對象功能強大,二是調試困難。但有時我們又不得不使用這種編程方式,來簡化我們的代碼,同時使增加可讀性、可拓展性等。

下面先放上我在模塊中使用的代碼:

Public Sub get_sudoku()nnSheet1.Range("A1:I9").ClearContentsnDim s As New SudokunCall s.creat_SudokunSet s = NothingnnEnd SubnnPublic Sub solute_sudoku()nnDim s As New SudokunCall s.creat_SudokunSet s = NothingnnEnd Subn

從方法名可以看出,第一個方法是隨機生成一個數獨,第二個方法是解答一個數獨。他們的核心代碼都只有三行,其實去掉最後一行也是沒問題的,然而一個數獨程序當然不可能靠兩行代碼就能輕鬆完成。

我們來解讀一下這幾行代碼都在說什麼,首先,Dim s As New Sudoku是將s聲明為一個新的類,這個類名叫Sudoku,就像將一個變數聲明為字元串類型那樣,我們把s稱為類Sudoku的一個實例。然後,Call s.creat_Sudoku是調用了Sudoku類型中的一個名為creat_Sudoku方法,創建了一個數獨。

那實際上的編程,我們是從創建這個Sudoku類開始的。

一、創建數獨類

在VBA工程窗口中,右鍵-插入-類模塊,很容易就新建了一個類。這時候我們最好先通過屬性窗口給類重命名,如果放任它是默認名類1的話,我們就只能通過Dim s As New 類1來實例化了。雖然VBA支持中文類名、方法、變數,但是用中文來編程還是覺得怪怪的。當然有時候也有用中文編程的優勢,比如EXCEL版的三國殺,作者幾乎全部使用中文來命名各種事件和人物,不然用翻譯過的南蠻入侵或者青釭劍,想想也是害人害己。

類模塊的樣子也和普通模塊沒什麼區別,但仔細一些就會發現,代碼窗口上方的兩個下拉菜單裡面多了一些內容。當左邊的選擇Class時,右邊會有Initialize和Terminate兩個選項,點選之後會分別創建Class_InitializeClass_Terminate兩個方法。這兩個是系統默認方法,不可以改名,也不可以增加變數,更沒法用Call指令調用,因為他們分別是在類被實例化被釋放時自從觸發。對應到上面的三行語句,就是在第一行和第三行被觸發。

註:由於VBA的類實例化方法無法傳入參數,所以一般有需求的話我們會自己寫一個Init方法,在實例化之後調用,只要不和系統方法重名就可以了。

創建了類之後我們先來給類添加公共變數,也就是這個類的屬性。類的屬性其實就是一個變數,只要類不被釋放就會一直存在,可以隨意更改和讀取。在VBA的類模塊我並不想引入封裝的概念,那樣只會使代碼更複雜難懂,雖然看起來更專業。

那麼數獨(Sudoku)這個類需要哪些屬性呢,我們可以把能想到的全局變數都寫上,想不全的話後期有需要再添加都可以。我能想到的有以下這些:1、一個計數器,記錄數獨生成的個數;2、一個棧,用來存放生成的數字,可隨時回溯;3、一個數組,用來存放每個單元格實時的狀態;4、三個條件數組,用來存放每行、每列、每個九宮格的約束條件。

註:一般在VBA中實現棧都是使用集合,因為不知道數組大小一直ReDim很不友好。但這裡數字大小是固定的,所以我們直接使用了數組。

代碼如下:

Option Explicitn強制顯式變數聲明nnPublic sum As IntegernnPublic num_arr As VariantnPublic cell_arr As VariantnnPublic row_list As VariantnPublic col_list As VariantnPublic box_list As Variantn

最上面的Option Explicit在模塊中是有必要的,因為大量的變數和屬性,一旦重名很容易錯亂。

我在這裡將數組都定義成Variant不定類型,因為常數、固定長度字元串、數組、用戶定義類型以及Declare語句不允許作為對象模塊的公共成員。定義數組的語句是會造成編譯錯誤的,而我們只能定義成不定類型,然後在類方法中重定義。

所有的類屬性都可以通過Me.sum = 0這樣的方法賦值,而即使重定義了數組也無法通過這種方式給數組中的元素賦值,只能通過一個臨時數組用以下方式賦值。

Dim temp_arr As Variantntemp_arr = Me.num_arrntemp_(1, 1) = 0nSet Me.num_arr = temp_arrn

這麼複雜的過程,我們即使寫成一個類方法也不是十分友好,而這種方式已經幾乎可以認為是封裝過程了。而實際上我們會用一種很微妙的方法解決這種尷尬,就是在給屬性中的數組賦值時不帶Me.這個部分,在其他語言中用慣了this.的話肯定會覺得很不習慣。然而這就是簡陋的VBA類模塊,所以強制顯式聲明變數變得十分有用,可以避免這種錯亂。

二、創建單元格類

上面提到一個cell_arr數組,需要用來存放每個單元格的實時狀態,因為一個單元格的狀態包含很多方面:基礎的行、列、九宮格的索引信息,單元格的初始狀態,以及不能存放的數字列表。

基於以上考慮,我們再插入一個類模塊,命名為Cell,並且聲明以下屬性:

Option ExplicitnnPublic row As IntegernPublic col As IntegernPublic box As IntegernPublic sum As IntegernnPublic status As IntegernPublic value As IntegernPublic unable_num_list As Variantn

一般來說,不能存放的數字列表unable_num_list,從存儲空間來考慮我們會用大小可變的集合或字典。在單元格內填數時,要確認數字是否存在與列表中,從速度方面來考慮我們會用字典的exists方法。但是,這正是造成了我第一次編程時速度過慢的原因。

因為字典在VBA中是一個對象,訪問對象的速度要遠慢於訪問數組,這也是為什麼我一般會把sheet中的range賦值給一個arr再進行操作,因為sheets也好ranges也好cells也好都是更為複雜的對象,訪問速度更加感人。

在數獨這個特殊數據環境中,數組大小是固定的9,存儲空間的問題並不算問題,而是否存在的遍歷最多也就是循環9次,並不會比exists方法慢太多。所以兩弊相權取其輕,我最後將代碼中的所有字典修改成了數組。數據類型選擇參考如下:

1、大量數據搜索時,字典速度優於數組

2、大量數據讀寫時,數組速度優於任何對象

3、一維數組讀寫速度優於高維數組

以上只是一個參考,也沒有必要過分優化,畢竟不是編程大賽,有時候一點差距並沒有太大影響,程序運行時無明顯卡頓就可以了。在這次數獨實現中為了直觀,也並沒有將二維數組降為一維處理。

三、創建類方法

單元格的類方法比較簡單, 基本就是在使用時賦值。我們可以在默認初始化方法中給不能存放的數字數組賦值:

Private Sub Class_Initialize()nnReDim unable_num_list(1 To 9) As IntegernDim in%nFor i = 1 To 9n unable_num_list(i) = 1nNext innEnd Subn

VBA中的布爾型變數True對應的整數為-1,False對應的整數為0,這裡統一用1和0來表示。

其他屬性的賦值我們需要自己寫一個方法:

Public Sub setValue(ByVal r As Integer, ByVal c As Integer, ByVal b As Integer, ByVal v As Integer, Optional ByVal s As Integer = 0)nnMe.row = rnMe.col = cnMe.box = bnnMe.sum = (r - 1) * 9 + cnMe.value = vnMe.status = snnunable_num_list(v) = 0nnEnd Subn

這裡分別傳入了行號、列號、九宮格號,然後根據行列算出了總個數(註:這就是二維降一維的方法),以及傳入的數值,和單元格初始狀態。最後將不能存放的數字數組中,該位置的值標記為0.

Cell類至此基本創建完成,對於一個類的基本創建和方法構建也做了一個大致的了解,下次繼續完成Sudoku類。


推薦閱讀:

Excel+VBA圖靈完備:Rule 110

TAG:VBA | Excel编程 |