Innlegg
Undersøkelse av argumentet for switch-setninger
6. desember 2015 • 9 min lesing

I nesten 50 år har switch-setningen (også kjent som case-setningen) vært en integrert del av programmering. I de senere årene hevder imidlertid noen at switch-setningen har overlevd sin nytte. Andre går enda lenger ved å stemple switch-setningen som en kode-lukt.
I 1952 unnfanget Stephen Kleene switch-setningen i sitt papir, Introduction to Metamathematics. Den første bemerkelsesverdige implementeringen var i ALGOL 58 i 1958. Senere ble switch-setningen inkludert i det uutslettelige C-programmeringsspråket, som, som vi vet, har påvirket de fleste moderne programmeringsspråk.
Spol frem til i dag og praktisk talt alle språk har en switch-setning. Imidlertid har noen få språk utelatt switch-setningen. Det mest bemerkelsesverdige er Smalltalk.
Dette vekket min nysgjerrighet, hvorfor ble switch-setningen ekskludert fra Smalltalk?
Andy Bower, en av skaperne/forkjemperne bak Dolphin Smalltalk delte sine tanker om hvorfor Smalltalk ekskluderte switch-setningen:
Da jeg først kom til Smalltalk fra C++, kunne jeg ikke forstå hvordan et angivelig fullverdig språk ikke støttet en switch/case-konstruksjon. Tross alt da jeg først flyttet opp til “strukturert programmering” fra BASIC trodde jeg at switch var en av de beste tingene siden skiveskåret brød. Men fordi Smalltalk ikke støttet en switch måtte jeg lete etter og forstå hvordan jeg kunne overvinne denne mangelen. Det riktige svaret er selvfølgelig å bruke polymorfisme og få objektene selv til å sende til riktig kodestykke. Da innså jeg at det ikke var en “mangel” i det hele tatt, men Smalltalk tvang meg inn i mye finere kornete OOP-design enn jeg hadde blitt vant til i C++. Hvis det hadde vært en switch-setning tilgjengelig ville det ha tatt meg mye lenger tid å lære dette eller, verre, jeg kunne fortsatt ha programmert C++/Java pseudo-objekt stil i Smalltalk.
Jeg vil hevde at i normal OOP er det ikke noe reelt behov for en switch-setning. Noen ganger, når man grensesnitter til en ikke-OOP verden (som å motta og sende WM_XXXX Windows-meldinger som ikke er objekter men bare heltall), da ville en switch-setning være nyttig. I disse situasjonene finnes det alternativer (som å sende fra en Dictionary) og antall ganger de dukker opp rettferdiggjør ikke inkludering av ekstra syntaks.
Hadde Andy rett? Har vi det bedre uten switch-setningen? Ville andre språk også ha nytte av å ekskludere switch-setningen?
For å kaste lys over dette spørsmålet har jeg satt sammen en sammenligning mellom en switch-setning, en dictionary og polymorfisme. La oss kalle det en oppgjør. Måtte den beste implementeringen vinne!
Hver implementering har en metode som tar en parameter, et heltall, og returnerer en streng. Vi vil bruke syklomatisk kompleksitet og vedlikeholdsindeks for å undersøke hver implementering. Vi vil deretter ta et helhetlig syn på alle tre implementeringene.
Koden.
Switch-setning
Vedlikeholdsindeks | 72 |
---|---|
Syklomatisk kompleksitet | 6 |
public class SwitchWithFourCases
{
public string SwitchStatment(int color)
{
var colorString = "Red";
switch (color)
{
case 1:
colorString = "Green";
break;
case 2:
colorString = "Blue";
break;
case 3:
colorString = "Violet";
break;
case 4:
colorString = "Orange";
break;
}
return colorString;
}
}
Dictionary
Vedlikeholdsindeks | 73 |
---|---|
Syklomatisk kompleksitet | 3 |
public class DictionaryWithFourItems
{
public string Dictionary(int color)
{
var colorString = "Red";
var colors = new Dictionary<int, string> {{1, "Green"}, {2, "Blue"}, {3, "Violet"}, {4, "Orange"}};
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
colorString = colors[color];
}
return colorString;
}
}
Polymorfisme
Total vedlikeholdsindeks | 94 |
---|---|
Total syklomatisk kompleksitet | 15 |
Grensesnitt
Vedlikeholdsindeks | 100 |
---|---|
Syklomatisk kompleksitet | 1 |
public interface IColor
{
string ColorName { get; }
}
Factory
Vedlikeholdsindeks | 76 |
---|---|
Syklomatisk kompleksitet | 4 |
public class ColorFactory
{
public string GetColor(int color)
{
IColor defaultColor = new RedColor();
var colors = GetColors();
var containsKey = colors.ContainsKey(color);
if (containsKey)
{
var c = colors[color];
return c.ColorName;
}
return defaultColor.ColorName;
}
private static IDictionary<int, IColor> GetColors()
{
return new Dictionary<int, IColor>
{
{1, new GreenColor()},
{2, new BlueColor()},
{3, new VioletColor()},
{4, new OrangeColor()},
{5, new MagentaColor()}
};
}
}
Implementering
Vedlikeholdsindeks | 97 |
---|---|
Syklomatisk kompleksitet | 2 |
public class BlueColor : IColor
{
public string ColorName => "Blue";
}
public class RedColor : IColor
{
public string ColorName => "Red";
}
public class GreenColor : IColor
{
public string ColorName => "Green";
}
public class MagentaColor : IColor
{
public string ColorName => "Magenta";
}
public class VioletColor : IColor
{
public string ColorName => "Violet";
}
Resultatene
Før jeg dykker ned i resultatene, la oss definere syklomatisk kompleksitet og vedlikeholdsindeks:
- Syklomatisk kompleksitet er målet på logisk forgrening. Jo lavere tallet, jo bedre.
- Vedlikeholdsindeks måler vedlikeholdbarhet av koden. Den er på en skala mellom 0 og 100. Jo høyere tallet, jo bedre.
Syklomatisk kompleksitet | Vedlikeholdsindeks | |
---|---|---|
Switch-setning | 6 | 72 |
Dictionary | 3 | 73 |
Polymorfisme | 15 | 94 |
Vi vil undersøke syklomatisk kompleksitet først.
Resultatene for syklomatisk kompleksitet er enkle. Dictionary-implementeringen er den enkleste. Betyr dette at det er den beste løsningen? Nei, som vi vil se når vi evaluerer vedlikeholdsindeksen.
De fleste ville tenke som jeg gjorde, implementeringen med lavest syklomatisk kompleksitet er den mest vedlikeholdbare — hvordan kunne det være annerledes?
I vårt scenario er ikke implementeringen med lavest syklomatisk kompleksitet den mest vedlikeholdbare. Faktisk i vårt scenario er det motsatt. Den mest komplekse implementeringen er den mest vedlikeholdbare! Sinn sprengt!
Hvis du husker, jo høyere vedlikeholdsindeks-score, jo bedre. For å komme til poenget, polymorfisme har den beste vedlikeholdsindeks-scoren — men den har også høyest syklomatisk kompleksitet. Hva skjer? Det virker ikke riktig.
Hvorfor er den mest komplekse implementeringen den mest vedlikeholdbare? For å svare på dette må vi forstå vedlikeholdsindeksen.
Vedlikeholdsindeksen består av 4 målinger: syklomatisk kompleksitet, kodelinjer, antall kommentarer og Halstead-volumet. De første tre målingene er relativt velkjente, men den siste, Halstead-volumet, er relativt ukjent. Som syklomatisk kompleksitet forsøker Halstead-volumet objektivt å måle kodekompleksitet.
I enkle termer måler Halstead-volum antall bevegelige deler (variabler, systemkall, aritmetikk, kodekonstruksjoner, osv.) i kode. Jo høyere antall bevegelige deler, jo mer kompleksitet. Jo lavere antall bevegelige deler, jo lavere kompleksitet. Dette forklarer hvorfor den polymorfe implementeringen scorer høyt på vedlikeholdsindeksen; klassene har få eller ingen bevegelige deler. En annen måte å se på Halstead-volumet er at det måler “bevegelige deler”-tetthet.
Hva er programvare, hvis ikke å endre? For å reflektere den virkelige verden introduserer vi endring. Jeg har lagt til en ny farge til hver implementering.
Nedenfor er de reviderte resultatene.
Syklomatisk kompleksitet | Vedlikeholdsindeks | |
---|---|---|
Switch-setning | 7 | 70 |
Dictionary | 3 | 73 |
Polymorfisme | 17 | 95 |
Switch-setningen og de polymorfe tilnærmingene økte begge i syklomatisk kompleksitet med en enhet, men interessant nok økte ikke dictionary. Først var jeg forvirret over dette, men så innså jeg at dictionary betrakter fargene som data og de andre to implementeringene behandler fargene som kode. Jeg kommer til saken.
Når vi retter oppmerksomheten mot vedlikeholdsindeksen, reduserte bare en, switch-setningen, i vedlikeholdbarhet. Polymorfismens vedlikeholdsscore forbedret seg og likevel øker også kompleksiteten (vi foretrekker at den reduseres). Som jeg nevnte ovenfor, er dette kontraintuitivt.
Vår sammenligning viser at dictionaries kan, fra et kompleksitetsstandpunkt, skalere uendelig. Den polymorfe tilnærmingen er langt den mest vedlikeholdbare og ser ut til å øke i vedlikeholdbarhet når flere scenarioer legges til. Switch-setningen øker i kompleksitet og reduseres i vedlikeholdbarhet når det nye scenarioet ble lagt til. Selv før vi la til det nye scenarioet hadde den verste syklomatiske kompleksitet og vedlikeholdsindeks-målinger.
Jem Finch fra Google delte sine tanker om switch-setningens mangler:
1. Polymorfe metodeimplementeringer er leksikalsk isolert fra hverandre. Variabler kan legges til, fjernes, modifiseres og så videre uten noen risiko for å påvirke urelatert kode i en annen gren av switch-setningen.
2. Polymorfe metodeimplementeringer er garantert å returnere til riktig sted, forutsatt at de terminerer. Switch-setninger i et fall-through språk som C/C++/Java krever en feilutsatt “break”-setning for å sikre at de returnerer til setningen etter switch i stedet for neste case-blokk.
3. Eksistensen av en polymorf metodeimplementering kan håndheves av kompilatoren, som vil nekte å kompilere programmet hvis en polymorf metodeimplementering mangler. Switch-setninger gir ingen slik fullstendighetssjekking.
4. Polymorf metodeutsendelse er utvidbar uten tilgang til (eller rekompilering av) annen kildekode. Å legge til en annen case til en switch-setning krever tilgang til den opprinnelige utsendelseskoden, ikke bare på ett sted men på alle steder den relevante enum blir switchet på.
5. … du kan teste polymorfe metoder uavhengig av switch-apparatet. De fleste funksjoner som switcher som eksemplet forfatteren ga vil inneholde annen kode som ikke kan testes separat; virtuelle metodekall, på den andre siden, kan.
6. Polymorfe metodekall garanterer konstant tid utsendelse. Ingen tilstrekkelig smart kompilator er nødvendig for å konvertere det som naturlig er en lineær tid konstruksjon (switch-setningen med fall through) til en konstant tid konstruksjon.
Dessverre, eller heldigvis, avhengig av din leir, har de fleste språk en switch-setning, og de forsvinner ikke med det første. Med dette i tankene er det godt å vite hva som skjer under panseret når man kompilerer switch-setninger.
Det er tre switch-setning optimaliseringer som kan oppstå:
- If-elseif setninger – Når en switch-setning har et lite antall cases eller spredte cases (ikke-inkrementelle verdier, som 10, 250, 1000) konverteres den til en if-elseif setning.
- Hopptabell – I større sett med tilstøtende cases (1, 2, 3, 4, 5) konverterer kompilatoren switch-setningen til en hopptabell. En hopptabell er i hovedsak en hashtabell med en peker (tenk goto-setning) til funksjonen i minnet.
- Binærsøk – For store sett med spredte cases kan kompilatoren implementere et binærsøk for å identifisere casen raskt, likt hvordan en indeks fungerer i en database. I ekstraordinære tilfeller hvor cases er et stort antall spredte og tilstøtende cases, vil kompilatoren bruke en kombinasjon av de tre optimaliseringene.
Sammendrag
I en objektorientert verden er switch-setningen, unnfanget i 1952, en grunnpilar for programvareutvikleren. Et bemerkelsesverdig unntak er Smalltalk hvor designerne valgte å ekskludere switch-setningen.
Når den sammenlignes med alternative ekvivalente implementeringer, dictionary og polymorfisme, klarte ikke switch-setningen seg like godt.
Switch-setningen er her for å bli, men som vår sammenligning har vist finnes det bedre alternativer til switch-setningen.
Implementeringene er tilgjengelige på Github.
Forfatter: Chuck Conway spesialiserer seg på programvareutvikling og Generativ AI. Koble til ham på sosiale medier: X (@chuckconway) eller besøk ham på YouTube.