問題描述

這個宏是為特定數量的 Civ V 玩家隨機抽取特定數量的文明選擇。它基於 43 種可能性的列表,while 循環中的 for 循環檢查已選擇的文明。恆定單元格引用指向工作表中使用 scroll-bars 獲得的值。使用基於變量的單元格引用來構造 two-column 設計中的結果。

有沒有什麼聰明的方式使我的代碼從美學和功能的角度更好?

Sub CIV_draw()
Range("K2:M50").ClearContents
Dim x As Integer
x = Cells(3, 3).Value
For i = 1 To x
    Cells(3 + (Cells(3, 7).Value + 2) * (i - 1), 11).Value = "Player " & i
    For j = 1 To Cells(3, 7).Value
        Dim Rand_CIV As String
        Dim RandNum As Integer
        Dim checkVal As Boolean
        checkVal = False
        While checkVal = False
            RandNum = CInt(Int(43 * Rnd())) + 1
            Rand_CIV = Cells(RandNum, 16).Value & " (" & Cells(RandNum, 15).Value & ")"
            For Z = 1 To 50
                If Cells(Z, 12) = Rand_CIV Then
                    Exit For
                End If
                If Z = 50 Then
                    checkVal = True
                End If
            Next Z
        Wend

        Cells((Cells(3, 7).Value + 2) * (i - 1) + (j + 3), 12).Value = Rand_CIV
    Next j
Next i
End Sub

我知道例如,我的重複檢查不是最佳的,我想獲得替代解決方案的建議。

最佳解決方法

那麼,Rubberduck 只表示一個問題。

Rubberduck Code Inspections – 3/8/2015 12:56:11 PM 1 issue found. Suggestion: Member ‘CIV_draw’ is implicitly Public – VBAProject.Module1, line 3

Implicit Public Member documentation

所以,你是一個好的開始。您可以通過顯式聲明子例程的範圍來糾正這一點。

Public Sub CIV_draw()

就我個人而言,我也會把這個前綴加在一起,只需調用這個例程 Draw()就可以了。下劃線在 VBA 中佔有特殊地位。它通常表示事件過程或接口實現,所以我將避免在其他地方的過程名稱中使用下劃線。


接下來我注意到,您正在調用 Range()Cells()方法,而不指定工作表。這意味着您在 ActiveSheet 上隱式調用這些方法。應該避免這種情況,因為在執行代碼時,用户將很少改變工作表。您應該創建一個工作表變量,該變量將在最初設置,然後使用它。

Dim ws As Worksheet
Set ws = ThisWorkbook.ActiveSheet

ws.Range("K2:M50").ClearContents
' ...

你的代碼中還有很多魔術數字,但是我會回頭看看。首先,我們來談談提取一些很有名的方法。隨機數邏輯是一個很好的解決方案。當讀這個方法時,我們並不在乎如何產生隨機數,只是我們得到一個。

RandNum = CInt(Int(43 * Rnd())) + 1 
Private Function GetRandomNum() As Integer
    GetRandomNum = CInt(Int(43 * Rnd())) + 1
End Function

接下來是獲得隨機文明字符串的業務。

Private Function GetRandomCivilization(ByVal RandNum As Integer, ByVal ws As Worksheet) As String
    GetRandomCivilization = ws.Cells(RandNum, 16).Value & " (" & ws.Cells(RandNum, 15).Value & ")"
End Function

這已經使您的代碼更高級的可讀性。如果有人想要細節,他們可以挖掘這些私人功能。我們也通過這樣做完全消除了一個變量。

Dim RandCivilization As String
Dim checkVal As Boolean

checkVal = False
While checkVal = False
    RandCivilization = GetRandomCivilization(GetRandomNum(), ws)
    For Z = 1 To 50

接下來我們來清理你的 While 循環。

Dim checkVal As Boolean  checkVal = False While checkVal = False 

接下來,讓我們翻轉邏輯來使用 Not 條件。

Dim checkVal As Boolean
checkVal = False

While Not checkVal

最後,給它一個更好的名字。

Dim endOfRange As Boolean
endOfRange = False

While Not endOfRange

下一個業務順序是將這個重複的邏輯提取到自己的方法中。

(ws.Cells(3, 7).Value + 2) 
Private Function GetNumberOfCivilizations(ByVal ws As Worksheet)
    GetNumberOfCivilizations = ws.Cells(3, 7).Value
End Function

這使得代碼的當前狀態為此。

Public Sub Draw()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.ActiveSheet

    ws.Range("K2:M50").ClearContents

    Dim x As Integer
    x = ws.Cells(3, 3).Value

    Dim numberOfCivs As Integer
    numberOfCivs = GetNumberOfCivilizations()

    Dim i As Long
    For i = 1 To x
        ws.Cells(3 + (numberOfCivs + 2) * (i - 1), 11).Value = "Player " & i

        Dim j As Long
        For j = 1 To numberOfCivs
            Dim RandCivilization As String
            Dim endOfRange As Boolean

            endOfRange = False
            While Not endOfRange
                RandCivilization = GetRandomCivilization(GetRandomNum(), ws)

                Dim z As Long
                For z = 1 To 50
                    If ws.Cells(z, 12) = RandCivilization Then
                        Exit For
                    End If
                    If z = 50 Then
                        endOfRange = True
                    End If
                Next z
            Wend

            ws.Cells((numberOfCivs + 2) * (i - 1) + (j + 3), 12).Value = RandCivilization
        Next j
    Next i
End Sub

Private Function GetNumberOfCivilizations(ByVal ws As Worksheet)
    GetNumberOfCivilizations = ws.Cells(3, 7).Value
End Function

Private Function GetRandomNum() As Integer
    GetRandomNum = CInt(Int(43 * Rnd())) + 1
End Function

Private Function GetRandomCivilization(ByVal RandNum As Integer, ByVal ws As Worksheet) As String
    GetRandomCivilization = ws.Cells(RandNum, 16).Value & " (" & ws.Cells(RandNum, 15).Value & ")"
End Function

這是一個改進,但我們仍然在處理很多數字,對我們來説並不意味着我們,直到我們回到實際的工作表。我們為它們定義一些常量值,以及一些負責從工作表獲取數據的函數。

Private Function GetNumberOfPlayers(ByVal ws As Worksheet) As Integer
    GetNumberOfPlayers = ws.Cells(3, 3).Value
End Function

還有另一個功能來獲取我們寫出結果的範圍。

Private Function GetResultsRange(ByVal ws As Worksheet) As Range
    Set GetResultsRange = ws.Range("K2:M50")
End Function

現在,請注意,我們正在引用結果範圍內的列位置,而不是相對於工作表的位置。一個人在只看代碼的時候更容易理解。

Public Sub Draw()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.ActiveSheet

    Dim resultsRange As Range
    Set resultsRange = GetResultsRange(ws)
    resultsRange.ClearContents

    Dim numOfPlayers As Integer
    numberOfPlayers = GetNumberOfPlayers()

    Dim numberOfCivs As Integer
    numberOfCivs = GetNumberOfCivilizations()

    Dim i As Long
    For i = 1 To numberOfPlayers
        resultsRange.Cells(3 + (numberOfCivs + 2) * (i - 1), 1).Value = "Player " & i

        Dim j As Long
        For j = 1 To numberOfCivs
            Dim RandCivilization As String
            Dim endOfRange As Boolean

            endOfRange = False
            While Not endOfRange
                RandCivilization = GetRandomCivilization(GetRandomNum(), ws)

                Dim z As Long
                For z = 1 To resultsRange.Rows.Count
                    If resultsRange.Cells(z, 2) = RandCivilization Then
                        Exit For
                    End If
                    If z = resultsRange.Rows.Count Then
                        endOfRange = True
                    End If
                Next z
            Wend

            resultsRange.Cells((numberOfCivs + 2) * (i - 1) + (j + 3), 2).Value = RandCivilization
        Next j
    Next i
End Sub

在這一點上,你應該得到這個想法。繼續提取良好命名的函數,直到您的主程序中沒有重複或模糊的邏輯。這個代碼段將是我的下一個目標。

(numberOfCivs + 2) * (i - 1) 

以及一些負責將數據寫入結果範圍的子站。


當我停止審查時,我的 IDE 中的代碼:

Option Explicit  Public Sub Draw()     Dim ws As Worksheet     Set ws = ThisWorkbook.ActiveSheet      Dim resultsRange As Range     Set resultsRange = GetResultsRange(ws)     resultsRange.ClearContents      Dim numOfPlayers As Integer     numberOfPlayers = GetNumberOfPlayers()      Dim numberOfCivs As Integer     numberOfCivs = GetNumberOfCivilizations()      Dim i As Long     For i = 1 To numberOfPlayers         resultsRange.Cells(3 + (numberOfCivs + 2) * (i - 1), 1).Value = "Player " & i          Dim j As Long         For j = 1 To numberOfCivs             Dim RandCivilization As String             Dim endOfRange As Boolean              endOfRange = False             While Not endOfRange                 RandCivilization = GetRandomCivilization(GetRandomNum(), ws)                  Dim z As Long                 For z = 1 To resultsRange.Rows.Count                     If resultsRange.Cells(z, 2) = RandCivilization Then                         Exit For                     End If                     If z = resultsRange.Rows.Count Then                         endOfRange = True                     End If                 Next z             Wend              resultsRange.Cells((numberOfCivs + 2) * (i - 1) + (j + 3), 2).Value = RandCivilization         Next j     Next i End Sub  Private Function GetNumberOfCivilizations(ByVal ws As Worksheet)     GetNumberOfCivilizations = ws.Cells(3, 7).Value End Function  Private Function GetRandomNum() As Integer     GetRandomNum = CInt(Int(43 * Rnd())) + 1 End Function  Private Function GetRandomCivilization(ByVal RandNum As Integer, ByVal ws As Worksheet) As String     GetRandomCivilization = ws.Cells(RandNum, 16).Value & " (" & ws.Cells(RandNum, 15).Value & ")" End Function  Private Function GetNumberOfPlayers(ByVal ws As Worksheet) As Integer     GetNumberOfPlayers = ws.Cells(3, 3).Value End Function  Private Function GetResultsRange(ByVal ws As Worksheet) As Range     Set GetResultsRange = ws.Range("K2:M50") End Function 

次佳解決方法

$O:$P 是應用程序數據,它們不完全屬於您的 front-end 工作表。

我將這兩列複製到另一個工作表,插入標題行,添加標題 CivilizationNameLeaderName,然後將該範圍轉換為表。是什麼賦予了?桌子是最有用的 Excel 的強大功能,也可以使用它們!

在 VBA 方面的事情,它大大簡化了訪問數據 – 您可以給工作表一個編程名稱,所以我將工作表命名為表 Civilizations,並將表本身命名為 tblCivilizations(“tbl” 前綴有用於區分 Excel 公式中不同類型的命名範圍) 。

這意味着 VBA 現在可以訪問這個表:

Dim civilizationsTable As ListObject
Set civilizationsTable = Civilizations.ListObjects(1)

然後可以使用 civilizationsTable.ListRows 迭代行,這意味着您可以在 i 行的第 2 列中獲取值,如下所示:

civilizationsTable.ListRows(i).Range(ColumnIndex:=2)

這意味着這樣一條線:

Rand_CIV = Cells(RandNum, 16).Value & " (" & Cells(RandNum, 15).Value & ")"

可以這樣寫:

civName = civilizationsTable.ListRows(RandNum).Range(ColumnIndex:=1)
civLeader = civilizationsTable.ListRows(RandNum).Range(ColumnIndex:=2)
civCaption = civLeader & " (" & civName & ")"

更多的代碼,你會説?等等,我還沒完成

Public Enum CivilizationTableColumns
    CivilizationName = 1
    CivilizationLeader '= 2
End Enum

把它變成這樣:

civName = civilizationsTable.ListRows(RandNum).Range(ColumnIndex:=CivilizationName)
civLeader = civilizationsTable.ListRows(RandNum).Range(ColumnIndex:=CivilizationLeader)
civCaption = civLeader & " (" & civName & ")"

然後我們可以買到一個 ListRow 對象:

Set row = civilizationsTable.ListRows(RandNum)
civName = row.Range(ColumnIndex:=CivilizatioName)
civLeader = row.Range(ColumnIndex:=CivilizationLeader)
civCaption = civLeader & " (" & civName & ")"

魔數什麼魔數? … 然後那個小塊可以被提取到自己的小功能中:

Private Function GetCivilizationCaption(ByVal index As Long)
    Set row = civilizationsTable.ListRows(index)
    civName = row.Range(ColumnIndex:=CivilizatioName)
    civLeader = row.Range(ColumnIndex:=CivilizationLeader)
    GetCivilizationCaption = civLeader & " (" & civName & ")"
End Function

這裏的外賣是因為缺乏抽象,很難看清楚發生了什麼。我們的小人類大腦喜歡抽象。證明 – 你寧願保持什麼,6 個月下來?

Cells((Cells(3, 7).Value + 2) * (i - 1) + (j + 3), 12).Value = Rand_CIV

要麼..

AssignCivilizationForPlayer player, civilizationCaption

參考文獻

注:本文內容整合自 Google/Baidu/Bing 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。