Som en stark förespråkare för versionskontroll i Microsoft Access måste jag prata om mitt största gnäll med VBA-utvecklingsmiljön:automatisk "omformning" av identifierare. Se detta som en utökning av mitt svar på en fråga om denna "funktion" på stackoverflow.
Jag kommer att närma mig den här artikeln i två delar. I del 1 kommer jag att definiera utvecklingsmiljöns beteende. I del 2 kommer jag att diskutera min teori om varför det fungerar på det här sättet.
Del 1:Definiera beteendet
Om du har spenderat någon tid på att skriva kod i VBA är jag säker på att du har lagt märke till denna "funktion". När du skriver in identifierare – variabler, funktionsnamn, enums, etc. – kanske du märker att IDE automatiskt ändrar skiftläge för dessa identifierare. Du kan till exempel skriva ett variabelnamn med alla små bokstäver, men så fort du flyttar till en ny rad växlar den första bokstaven i variabeln plötsligt till versaler.
Första gången du ser det här kan det vara jobbigt. När du fortsätter programmera, fortsätter IDE att ändra fallet på dig till synes slumpmässigt. Men om du spenderar tillräckligt med tid i IDE, så avslöjar mönstret sig så småningom.
För att rädda dig från att behöva spendera över tio år av ditt liv i väntan på att mönstret ska visa sig för dig, kommer jag nu att beskriva mönstret så som jag har förstått det. Såvitt jag vet har Microsoft aldrig officiellt dokumenterat något av detta beteende.
- Alla automatiska falländringar är globala för VBA-projektet.
- När deklarationsraden för någon av följande typer av identifierare ändras, ändras även skiftläge för alla andra identifierare med samma namn:
- Undernamn
- Funktionsnamn
- Skriv namn
- Enum-namn
- Variabelnamn
- Konstant namn
- Egendomens namn
- När ett enum-objekts namn ändras någonstans i koden, uppdateras skiftläget för enum-objektets namn för att matcha överallt.
Låt oss prata om vart och ett av dessa beteenden lite mer detaljerat nu.
Globala förändringar
Som jag skrev ovan är förändringar av identifierarfall globala för ett VBA-projekt. Med andra ord ignorerar VBA IDE helt omfattningen när den ändrar fallet med identifierare.
Låt oss till exempel säga att du har en privat funktion som heter AccountIsActive i en standardmodul. Föreställ dig nu en klassmodul någon annanstans i samma projekt. Klassmodulen har en privat Property Get-procedur. Inuti den Property Get-proceduren finns en lokal variabel som heter accountIsActive . Så fort du skriver raden Dim accountIsActive As Boolean
in i VBA IDE och flytta till en ny rad, funktionen AccountIsActive som vi definierade separat i sin egen standardmodul har dess deklarationsrad ändrad till Private Function accountIsActive()
för att matcha den lokala variabeln i denna klassmodul.
Det är en munfull, så låt mig visa det bättre i kod.
Steg 1:Definiera AccountIsActive-funktionen
'--== Module1 ==--
Private Function AccountIsActive() As Boolean
End Function
Steg 2:Deklarera accountIsActive lokal variabel i olika omfattning
'--== Class1 ==--
Private Sub Foo()
Dim accountIsACTIVE As Boolean
End Sub
Steg 3:VBA IDE...vad har du gjort?!?!
'--== Module1 ==--
Private Function accountIsACTIVE() As Boolean
End Function
VBA Case-Obliterations policy för icke-diskriminering
VBA nöjer sig inte med att helt enkelt ignorera räckvidden, utan ignorerar också skillnader mellan olika typer av identifierare i sin strävan att införa konsistens i höljet. Med andra ord, varje gång du deklarerar en ny funktion, subrutin eller variabel som använder ett befintligt identifierarnamn, ändras alla andra instanser av den identifieraren för att matcha.
I vart och ett av dessa exempel nedan är det enda jag ändrar den första modulen som anges. VBA IDE är ansvarig för alla andra ändringar av tidigare definierade moduler.
Steg 1:Definiera en funktion
'--== Module1 ==--
Public Function ReloadDBData() As Boolean
End Function
Steg 2:Definiera en sub med samma namn
OBS:Detta är helt giltigt så länge som procedurerna finns i olika moduler. Som sagt, bara för att du *kan* göra något, betyder det inte att du *bör*. Och du *bör* undvika den här situationen om det alls är möjligt.
'--== Module2 ==--
Public Sub ReloadDbData()
End Sub
'--== Module1 ==--
Public Function ReloadDbData() As Boolean
End Sub
Steg 3:Definiera en typ med samma namn
OBS:Återigen, vänligen definiera inte en sub, funktion och typ alla med samma namn i ett enda projekt.
'--== Module3 ==--
Private Type ReLoadDBData
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub ReLoadDBData()
End Sub
'--== Module1 ==--
Public Function ReLoadDBData() As Boolean
End Sub
Steg 4:Definiera en enum med samma namn
OBS:Snälla, snälla, snälla, för kärleken till allt heligt...
'--== Module4 ==--
Public Enum ReloadDbDATA
Dummy
End Enum
'--== Module3 ==--
Private Type ReloadDbDATA
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub ReloadDbDATA()
End Sub
'--== Module1 ==--
Public Function ReloadDbDATA() As Boolean
End Sub
Steg 5:Definiera en variabel med samma namn
OBS:Vi gör faktiskt fortfarande det här?
'--== Module5 ==--
Public reloaddbdata As Boolean
'--== Module4 ==--
Public Enum reloaddbdata
Dummy
End Enum
'--== Module3 ==--
Private Type reloaddbdata
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub reloaddbdata()
End Sub
'--== Module1 ==--
Public Function reloaddbdata() As Boolean
End Sub
Steg 6:Definiera en konstant med samma namn
OBS:Åh, kom igen. Allvarligt?
'--== Module6 ==--
Private Const RELOADDBDATA As Boolean = True
'--== Module5 ==--
Public RELOADDBDATA As Boolean
'--== Module4 ==--
Public Enum RELOADDBDATA
Dummy
End Enum
'--== Module3 ==--
Private Type RELOADDBDATA
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub RELOADDBDATA()
End Sub
'--== Module1 ==--
Public Function RELOADDBDATA() As Boolean
End Sub
Steg 7:Definiera en klassegenskap med samma namn
OBS:Det här börjar bli dumt.
'--== Class1 ==--
Private Property Get reloadDBData() As Boolean
End Property
'--== Module6 ==--
Private Const reloadDBData As Boolean = True
'--== Module5 ==--
Public reloadDBData As Boolean
'--== Module4 ==--
Public Enum reloadDBData
Dummy
End Enum
'--== Module3 ==--
Private Type reloadDBData
Dummy As Variant
End Type
'--== Module2 ==--
Public Sub reloadDBData()
End Sub
'--== Module1 ==--
Public Function reloadDBData() As Boolean
End Sub
Enum artiklar?!?!
För denna tredje punkt är det viktigt att skilja mellan en Enum-typ och ett Enum-objekt .
Enum EnumTypeName ' <-- Enum type
EnumItemAlice ' <-- Enum item
EnumItemBob ' <-- Enum item
End Enum
Vi har redan visat ovan att Enum-typer behandlas på samma sätt som andra typer av deklarationer, som subs, funktioner, konstanter och variabler. Närhelst deklarationsraden för en identifierare med det namnet ändras, uppdateras varannan identifierare i projektet med samma namn för att matcha den senaste ändringen.
Uppräkna objekt är speciella genom att de är den enda typen av identifierare vars skiftläge kan ändras när en kodrad som helst som innehåller enum-objektets namn ändras.
Steg 1. Definiera och fyll i Enum
'--== Module7 ==--
Public Enum EnumTypeName
EnumItemAlice
EnumItemBob
End Enum
Steg 2. Se Enum-posterna i koden
'--== Module8 ==--
Sub TestEnum()
Debug.Print EnumItemALICE, EnumItemBOB
End Sub
Resultat:Enum-typdeklarationen ändras för att matcha vanlig kodrad
'--== Module7 ==--
Public Enum EnumTypeName
EnumItemALICE
EnumItemBOB
End Enum
Del 2:Hur kom vi hit?
Jag har aldrig pratat med någon i det interna VBA-utvecklingsteamet. Jag har aldrig sett någon officiell dokumentation om varför VBA IDE fungerar som den gör. Så det jag ska skriva är rena gissningar, men jag tycker att det är vettigt.
Under en lång tid undrade jag varför i hela världen VBA IDE skulle ha detta beteende. Det är trots allt helt klart avsiktligt. Det enklaste för IDE att göra skulle vara...ingenting. Om användaren deklarerar en variabel med stora bokstäver, låt den stå i stora bokstäver. Om användaren sedan refererar till variabeln med gemener några rader senare, lämna den referensen med gemener och den ursprungliga deklarationen med stora bokstäver.
Detta skulle vara en helt acceptabel implementering av VBA-språket. Trots allt är språket i sig skiftlägesokänsligt. Så varför göra allt besväret för att automatiskt ändra identifierarens hölje?
Ironiskt nog tror jag att motivationen var att undvika förvirring. (Swing and a miss, om du frågar mig.) Jag hånar den här förklaringen, men det är vettigt.
Kontrast mot skiftlägeskänsliga språk
Låt oss först prata om programmerare som kommer från ett skiftlägeskänsligt språk. En vanlig konvention i skiftlägeskänsliga språk, som C#, är att namnge klassobjekt med stora bokstäver och att namnge instanser av dessa objekt med samma namn som klassen, men med en ledande liten bokstav.
Den konventionen kommer inte att fungera i VBA, eftersom två identifierare som endast skiljer sig åt i skiftläge anses vara likvärdiga. Faktum är att Office VBA IDE låter dig inte samtidigt deklarera en funktion med en typ av hölje och en lokal variabel med en annan typ av hölje (vi täckte detta uttömmande ovan). Detta hindrar utvecklaren från att anta att det finns en semantisk skillnad mellan två identifierare med samma bokstäver men olika skiftläge.
Få fel kod att se fel ut
Den troligare förklaringen i mina ögon är att denna "funktion" finns för att få likvärdiga identifierare att se identiska ut. Tänk på det; utan den här funktionen skulle det vara lätt för stavfel att leda till körtidsfel. Tro mig inte? Tänk på detta:
Private mAccountName As String
Private Const ACCOUNT_NAME As String = "New User"
Private Sub Class_Initialize()
mAccountName = ACCOUNT_NAME
End Sub
Public Property Get MyAccountName() As String
MAccountName = Account_Name
End Property
Public Property Let MyAccountName(AccountName As String)
mAccountName = Account_Name
End Property
Om du tittar snabbt på ovanstående kod ser det ganska okomplicerat ut. Det är en klass med .Mitt kontonamn fast egendom. Medlemsvariabeln för egenskapen initieras till ett konstant värde när objektet skapas. När kontonamnet ställs in i kod uppdateras medlemsvariabeln igen. När egenskapsvärdet hämtas, returnerar koden bara innehållet i medlemsvariabeln.
Åtminstone är det vad den ska göra. Om jag kopierar ovanstående kod och klistrar in den i ett VBA IDE-fönster blir höljet på identifierarna konsekvent och runtime-buggarna visar sig plötsligt:
Private mAccountName As String
Private Const ACCOUNT_NAME As String = "New User"
Private Sub Class_Initialize()
mAccountName = ACCOUNT_NAME ' <- This is OK
End Sub
Public Property Get MyAccountName() As String
mAccountName = ACCOUNT_NAME ' <- This is probably not what we intended
End Property
Public Property Let MyAccountName(AccountName As String)
mAccountName = ACCOUNT_NAME ' <- This is definitely not what we meant
End Property
Implementering:Är detta verkligen det bästa tillvägagångssättet?
Umm, nej. Missförstå mig inte. Jag gillar faktiskt idén att automatiskt ändra versaler för identifierare för att bibehålla konsekvens. Mitt enda verkliga klagomål är att ändringen görs till varje identifierare med det namnet i hela projektet. Mycket bättre skulle vara att ändra versaler för endast de identifierare som refererar till samma "sak" (oavsett om den "saken" är en funktion, sub, egenskap, variabel, etc.).
Så varför fungerar det inte så här? Jag förväntar mig att VBA IDE-utvecklarna håller med om mitt perspektiv på hur det ska fungera. Men det finns en mycket god anledning varför IDE inte fungerar på det sättet. Med ett ord, prestanda.
Tyvärr finns det bara ett tillförlitligt sätt att upptäcka vilka identifierare med samma namn som faktiskt refererar till samma sak:analysera varje rad kod. Det är sloooowwwwww. Detta är mer än en enkel hypotes från min sida. Rubberduck VBA-projektet gör faktiskt precis detta; den analyserar varje rad med kod i projektet så att den kan göra automatiserad kodanalys och en massa andra coola grejer.
Projektet är visserligen tungt. Det fungerar förmodligen utmärkt för Excel-projekt. Tyvärr har jag aldrig haft tillräckligt med tålamod för att använda det i något av mina Access-projekt. Rubberduck VBA är ett tekniskt imponerande projekt, men det är också en varnande berättelse. Att respektera räckvidden när man ändrar versaler för identifierare skulle vara bra att ha, men inte på bekostnad av VBA IDE:s nuvarande blixtrande snabba prestanda.
Slutliga tankar
Jag förstår motivet för denna funktion. Jag tror att jag till och med förstår varför det implementeras som det är. Men det är den enskilt mest irriterande egenheten i VBA för mig.
Om jag kunde ge en enda rekommendation till Office VBA-utvecklingsteamet skulle det vara att erbjuda en inställning i IDE för att inaktivera automatiska falländringar. Det nuvarande beteendet kan förbli aktiverat som standard. Men för avancerade användare som försöker integrera med versionskontrollsystem kan beteendet inaktiveras helt för att förhindra störande "kodändringar" från att förorena revisionshistoriken.