Den mærkelige SWITCH-statement og hvorfor jeg holder af den

Den er en del af den fundamentale værktøjskasse i mange sprog, men switch skiller sig også ud, og der er dem, der mener, den er overflødig.

»Hvorfor har du et break i hvert af dine case-udtryk? Er det rigtigt?«

Nogenlunde sådan lød spørgsmålet fra en kollega henover skrivebordene på redaktionen. I virkeligheden lød det nok mindre pænt og mere anklagende, men under alle omstændigheder endte jeg med at måtte forklare, hvorfor switch er sådan en sær størrelse. Altså en god dag på kontoret.

Nu havde jeg brugt en switch i et script, jeg havde lavet, som skulle gøre det nemt at konvertere tal til superscript og subscript ved hjælp af Unicode, som vores database for nylig blev opdateret til at understøtte.

Heldigvis havde jeg læst dette grundige blogindlæg om 'The curious case of the switch statement', så jeg var klædt på til at imødegå kritikken og hurtigt svare, at ja, det er helt godnat med de der breaks.

Switch er lidt et problembarn i de fleste programmeringssprog. I modsætning til basal logik som if eller løkker som for og while, så er switch en mærkelig blanding, der hverken er det ene eller det andet, og man kan opnå samme resultat uden af gøre brug af switch.

Min switch var i Javascript, som følger den mest udbredte måde at skrive en switch på, og det store problem er, at i de fleste sprog har switch en syntaks, der er lige en lille smule ved siden af, hvad man ville forvente i forhold til den gængse syntaks i sproget.

Det bliver noget med case og så hverken parenteser eller klammer, men et kolon, og hver case afsluttes med et break, som ikke helt er et rigtigt break, men i stedet fremstår som noget, der er efterladt fra en arkaisk kodestil.

Jeg er altid nødt til lige at slå syntaksen op, når jeg kaster mig ud i en switch.

Switchen er da også lidt arkaisk med rødder tilbage til sprog, hvor alt blev skrevet med store bogstaver.

Den dufter dog dejligt af at spare på nogle helt basale maskininstruktioner ved at bruge noget, der nærmest er labels og goto'er, på en måde, der virker meget forbudt for dem, der kommer fra de sprog, hvor sådan noget regnes for at være uartigt.

I mit javascript kunne jeg da også have løst opgaven med bare min for-løkke og et passende Regex-udtryk eller en dictionary og noget if-halløj. Det var imidlertid en klassisk case for en switch.

Det, jeg godt kan lide ved en switch, er, at den viser, hvad den gør. Ok, syntaksen er noget rod, men når man overvinder dét, så er en switch let at læse.

Og jeg føler, at jeg instruerer computeren i at gøre noget, i modsætning til sådan noget som this.getSomething(that.doThisFirst(ImAValueOrMaybeAnObject)) hvor jeg mest krydser fingre for, at jeg har forstået dokumentationen korrekt, og nogen sørger for, at jeg har brugt nok parenteser.

Det, jeg er mindre vild med i min egen implementering, er, at jeg har gentaget næsten enslydende kode over 10 afsnit. Det kunne jeg selvfølgelig have rykket ud i en funktion for sig selv - på bekostning af lidt læsbarhed.

Der findes erklærede switch-modstandere, og den er da også lidt bøvlet. Men try-catch er også bøvlet og mærkeligt. Og én eller anden burde have sagt stop, da nogen i fuld alvor foreslog === som en løsning på noget som helst.

Tips og korrekturforslag til denne historie sendes til tip@version2.dk
Kommentarer (31)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Yoel Caspersen Blogger

Jeg tror, det er første gang jeg er stødt på en journalist, der aktivt koder - og så på et IT-medie? Respekt!

Jeg har det lidt ligesom dig, når det kommer til en switch - det er en mærkelig konstruktion, som dog alligevel har sine fordele, hvis man holder sine cases på meget få linier.

I C/C++ er der dog et issue med brugervenligheden - hvor man i PHP kan bruge switch på en streng, er det ikke muligt i C/C++. Det er nok dybest set et problem med strenghåndtering i C/C++ mere end det har noget med switch at gøre.

try-catch er takket være Java brugt i alt for mange sammenhænge. En exception bør IMHO ikke være en del af et almindeligt kontrol-flow, da det jo netop er en undtagelse fra det normale.

  • 6
  • 0
Lars Skovlund

I C/C++ er der dog et issue med brugervenligheden - hvor man i PHP kan bruge switch på en streng, er det ikke muligt i C/C++. Det er nok dybest set et problem med strenghåndtering i C/C++ mere end det har noget med switch at gøre.


Nej, det er fordi alle cases i en switch skal være repræsenteret ved en (numerisk) konstant. Sammenligning af strenge i C/C++ kræver ganske rigtigt et funktionskald, og derfor kan man ikke switche på en streng. Det har ikke mindst at gøre med kodeoptimering (hvis alle cases er numeriske konstanter, kan en switch implementeres med et opslag i en tabel samt et hop). I nogle programmeringssprog har man både switch som i C/C++ og en mere generel, men med hvert sit statement; det kan godt være jeg er gammeldags, men jeg kan nu godt lide det sådan.

  • 1
  • 0
Mathias Hagensen

Som med alt muligt kode, kan SWITCH bruges rigtigt og forkert. En af de største styrker er, at det rent faktisk er læselig kode. Det mest upraktiske er som nævnt break som skal skrives mere end nødvendigt. Jeg ville fortrække en syntaks a la:

switch(key) {
case 1 || 2 || 3: text = 'Label 1';
case 4: text = 'Label 2';
default: text = 'Label 3';
}

Dvs. mulighed for flere værdier i case og så fjerner vi den grimme break

  • 0
  • 1
Dennis Krøger

Switches har en helt legitim funktion, men med mindre jeg har misforstået noget er din brug af switch helt hen i vejret. Hvad er der galt med

for (i = 0; i < seltext.length; i++) {  
    var subIndex = parseInt(seltext[i]);  
    if(!isNaN(subIndex)) {  
        leftsel = subbedtext.slice(0,i);  
        rightsel = subbedtext.slice(i+1);  
        subbedtext = leftsel + unisubchars[subIndex] + rightsel;  
    }  
}

?

Det har også den fordel at det kommunikerer hvad du vil.

Hvis du absolut vil have den switch kunne du i det mindste rykke leftsel og rightsel uden for, der er ingen grund til at gentage dem for hver case.

  • 3
  • 0
Erlo Haugen

Ada:

case X is  
   when 1 =>  
      Walk_The_Dog;  
   when 5 =>  
      Launch_Nuke;  
   when 8 | 10 =>  
      Sell_All_Stock;  
   when others =>  
      Self_Destruct;  
end case;

Alle de mulige værdier af X skal være dækket i case-sætningen. X kan være en heltalstype eller en enum-type (De er forskellige typer i Ada).

  • 2
  • 0
Yoel Caspersen Blogger

Sammenligning af strenge i C/C++ kræver ganske rigtigt et funktionskald, og derfor kan man ikke switche på en streng.

Strenghåndtering har altid været et smertensbarn i C, og C++ er ikke meget bedre på det punkt. I C er en streng jo bare en pointer til et byte array, og enden på strengen findes ved en null-byte. Det giver alskens problemer, lige fra off-by-one-errors til buffer overflow-fejl og dårlig performance.

Så kan jeg bedre lide strenghåndteringen i Pascal, hvor længden på strengen er angivet i starten - det fjerner alle disse problemer.

Det har ikke mindst at gøre med kodeoptimering (hvis alle cases er numeriske konstanter, kan en switch implementeres med et opslag i en tabel samt et hop).

Det giver rigtig god mening - men programmeringssproget er jo et "human interface" til koden, og derfor burde det være compilerens opgave at optimere - og hvis man laver en switch på numeriske konstanter er performance så blot højere end hvis man gør det på en streng. Igen, strenghåndtering er ikke en naturlig del af programmeringssproget i C/C++.

  • 1
  • 0
Jesper Stein Sandal

Jeg siger ikke, at min implementering er den bedste*. Så tak for dit forslag! :)

Men jeg ville oprindeligt gerne gøre metoden generel nok til fx at kunne bruge den til at skrive xⁿ og NOₓ. Det er derfor, jeg ikke prøver at parse en int.

Mht. placeringen af slicen, så har du ret i, at den kunne placeres uden for, men jeg er nok opdraget til ikke at lave den slags, hvis det ikke skal bruges. Tanken er, at man kan markere et helt ord som eksempelvis C2H5OH og så få C₂H₅OH og kun slice to gange. Det er ikke sikkert, at det er bedre end bare at slice hver gang (hvis jeg har forstået dit forslag rigtigt), og min måde gør koden længere end nødvendigt.

Anyway, så understreger det bare pointen om, at man altid kan løse opgaven uden at bruge en switch. :)

*) Ok, den hurtige implementering, der virker uden væsentlig omkostning i performance eller vedligeholdelse, er næsten altid den bedste implementering, også selvom der findes en smukkere implementering X.

  • 0
  • 0
Casper Pedersen

For nogle år siden, over et (par) glas vin havde jeg en længere diskussion med en af arkitekterne af Python ... mit argument var at Python ikke er en "fornuftigt" sprog da det ikke indeholder en switch/case, man er nød til at lave alt med 'if' eller dictonaries hvilket gør det ovenstående eksempel til noget ulæseligt rod hvis man bruger Python.

Se for eksempel her: https://www.pydanny.com/why-doesnt-python-have-switch-case.html

Som vi ved så blev PEP 3103 (https://www.python.org/dev/peps/pep-3103/) fordi arkitekterne ikke kunne nogen grund til at sproget skulle have dette ...

Jeg bruger stadigvæk Python til forskellige ting, men holder stadigvæk på at sproget aldrig bliver voksen uden en switch/case - og ja jeg ved at der er mange der holder på noget andet.

  • 0
  • 0
Lars Skovlund

Jeg er selv oplært på Pascal, der måtte man heller ikke switche på en streng. Altså, hvis det bare er det, så kan man måske godt argumentere for at det bør være muligt - men hvor stopper det? Man har jo f.eks. heller ikke lov at skrive

int bar, baz;  
   
void test(int foo)  
{  
  switch (foo)  
  {  
    case bar : ...  
    case baz : ...  
  }  
}

Hvis man tillader funktionskald (til strcmp, eksempelvis) kan man vel også argumentere for at tillade ovenstående...? Der mener jeg, at det har en vis forebyggende effekt at der kun må forekomme konstanter der. Så jeg synes det er godt nok som det er.

  • 0
  • 0
Dennis Krøger

Men jeg ville oprindeligt gerne gøre metoden generel nok til fx at kunne bruge den til at skrive xⁿ og NOₓ. Det er derfor, jeg ikke prøver at parse en int.


Ah, så giver den del lidt mere mening, selvom at bruge et objekt som et dictionary eller en anden måde at lave et direkte opslag nok stadig ville være mere læsbart end array + switch.

Mht. placeringen af slicen, så har du ret i, at den kunne placeres uden for, men jeg er nok opdraget til ikke at lave den slags, hvis det ikke skal bruges. Tanken er, at man kan markere et helt ord som eksempelvis C2H5OH og så få C₂H₅OH og kun slice to gange. Det er ikke sikkert, at det er bedre end bare at slice hver gang (hvis jeg har forstået dit forslag rigtigt), og min måde gør koden længere end nødvendigt.


Hovedproblemet er ikke performance, mere at du gentager koden igen og igen. Hvis du på et tidspunkt skal ændre noget, er du både ude i at det er mere arbejde end det behøver at være, og at der er risiko for at du misser en. Og så har du pludselig en fejl der kan være både svær at debugge (det virker i 90% af tilfældene), og svær at se.

Mit forslag er ganske enkelt bare

                for (i = 0; i < seltext.length; i++) {  
                    leftsel = subbedtext.slice(0,i);  
                    rightsel = subbedtext.slice(i+1);  
                    switch(seltext[i]) {  
                        case "0":  
                            subbedtext = leftsel + unisubchars[0] + rightsel;  
                            break;  
                        case "1":  
                            subbedtext = leftsel + unisubchars[1] + rightsel;  
                            break;  
                        case "2":  
                            subbedtext = leftsel + unisubchars[2] + rightsel;  
                            break;  
                        case "3":  
                            subbedtext = leftsel + unisubchars[3] + rightsel;  
                            break;  
                        case "4":  
                            subbedtext = leftsel + unisubchars[4] + rightsel;  
                            break;  
                        case "5":  
                            subbedtext = leftsel + unisubchars[5] + rightsel;  
                            break;  
                        case "6":  
                            subbedtext = leftsel + unisubchars[6] + rightsel;  
                            break;  
                        case "7":  
                            subbedtext = leftsel + unisubchars[7] + rightsel;  
                            break;  
                        case "8":  
                            subbedtext = leftsel + unisubchars[8] + rightsel;  
                            break;  
                        case "9":  
                            subbedtext = leftsel + unisubchars[9] + rightsel;  
                            break;  
                        default:  
                            break;  
                    }  
                }

Funktionelt er der ingen forskel.

  • 0
  • 0
Bjørn Engsig

C/C++ er som de er mht. strenge, og det har ikke noget med switch at gøre. Problemet er, at "break" har to betydninger, hvor den ene er at afbryde en løkke, og den anden er at afslutte en case. Resultatet er, at såfremt man har en løkke med en switch indeni, bliver man nødt til at anvende goto og en label, såfremt man ønsker at afbryde løkken inde fra switchen:

for (;;)  
{  
  switch (x)  
  {  
    case NUERDETNOK:  
    goto udaffor;  
  }  
}  
udaffor:
  • 5
  • 0
Carsten Hansen

Break konstruktionen er fra en tid, hvor det første program var en parser.

Case 'a':
Case 'b':
..
Case 'Z': tokentype = LETTER;

Selvom konstruktionen måske kun skulle bruges en gang per program, så kunne det have stor betydning for hastigheden, da den blev anvendt mange gange. Hvis hastigheden ikke var i top, så var der nok nogle programmører, der ville foretrække Assembler.

Derudover kan SWITCH laves om til en GOTO(funcPtrArray[caseIndex]) i nogle situationer under kompilering.

  • 0
  • 0
Jesper Stein Sandal

Det har du ret i - især fordi jeg let kommer til at bytte om på højre og venstre - og det bliver ikke lettere af at have både noget med sup, sub og så sub igen (for substitute, ikke subscript), så jeg har virkelig givet mig selv gode muligheder for at lave fejl, hvis jeg skal opdatere koden på et tidspunkt. :)

  • 0
  • 0
Gustav Brock

Joh, jeg tror ikke, der findes en eneste programmør, der ikke har studset over den omstændige syntaks. Og der har været utallige diskussioner som denne. Og man har for længst puttet det hen i hjørnet med mærkværdigheder, der ikke er større, end at de er til at leve med.

Men er det virkelig dagens tophistorie på Version2?

  • 0
  • 5
Baldur Norddahl

Sprog designet i en bestemt periode ser ud til at have en "switch" statement som er meget inspireret af C. Nyere sprog designet indenfor de sidste 10 år har en tendens til i stedet at have pattern matching.

Pattern matching kan alt det en "switch" kan bare uden begrænsninger og typisk også uden "break". Hvis dine patterns tilfældigvis er heltal i et begrænset range, så kan compileren godt genkende mønsteret og implementere det ved et tabel lookup, så man behøver ikke begrænse sproget på grund af en optimeringsmulighed ved et bestemt mønster.

Eksempel på pattern matching i Scala:

sealed trait FooBar  
case class Foo(a: Int, b: String) extends FooBar  
case class Bar(c: String) extends FooBar  
   
def fooBarText(obj: FooBar) = obj match {  
  case Foo(a,b) => f"Foo has a=$a and b=$b"  
  case Bar(c) => f"Bar has c=$c"  
}  
   
// pattern matching med regular expressions  
val Re1 = """Foo has a=(\d+) and b=(.*)""".r  
val Re2 = """Bar has c=(.*)""".r  
   
def textToFooBar(str: String): Option[FooBar] = str match {  
  case Re1(a,b) => Some(Foo(a.toInt,b))  
  case Re2(c) => Some(Bar(c))  
  case _ => None  
}
  • 1
  • 0
Jacob Rasmussen

De der foreslår at man omskriver syntaxen for en switch, til ikke at indeholde et obligatorisk break i slutningen af hver case, glemmer at nogle sprog tillader fall-through til næste case.

    switch(myInt){  
        case 0:   
            a();  
        case 1:   
            b();  
            break;  
        case 2:  
            c();  
            break;  
   }

I ovenstående tilfælde vil 0 resultere i at 'a' og 'b' bliver udført, 1 vil kun udføre 'b' og 2 vil kun udføre 'c'.

  • 5
  • 0
Mathias Hagensen

Jeg har ikke glemt det. Jeg mener bare at det bliver sjældent brugt og i de tilfælde det bliver brugt, leder det oftest til fejl ved vedligeholdelsen, fordi man glemmer at tjekke om man påvirker noget der kommer ovenfra - det er selvfølgelig bare en personlig holdning.

  • 2
  • 0
Casper Niebe

Jeg har altid fundet det morsomt, at man kan flytte evalueringen fra switch-statement'et til de enkelte cases i PHP. Eksempelvis:

switch (true) {  
  case ($var == "en streng"):  
    print "something";  
    break;  
  case ($andenvar == "noget helt andet"):  
    print "Noget markant andet, jeg har testet på";  
    break;  
  default:   
    print "Alt andet end ovenstående";  
    break;  
}

Og undlader man breaks, kan man bare lade switchen høvle ned gennem alle tests :) Det er super-grimt og i manges verden et komplet no-go, men det er en spøjs sproglig konstruktion, der mig bekendt ikke er muligt i andre sprog?

  • 2
  • 0
Jan Gundtofte-Bruun

Helt enig, at bruge break mellem cases er et sted imellem at være noget klyt og noget som sjældent bliver udnyttet "med vilje" (som Mathias også skriver).

Meen, min holdning stammer nok fra, at jeg startede med Pascal (nærmere betegnet Macintosh ANSI Pascal v1.0), hvor "switch" (der godt nok hed "select") ikke havde noget "break" mellem cases -- men hvor hver case kunne være en expression a la "case <10" eller "case 11..25", så det var meget nemmere at lave "ranged cases".

  • 0
  • 0
Ivan Skytte Jørgensen

En gang for mange år siden da jeg lavede en opgave på edb-assistent-uddannelse var sproget Informix 4GL. Det havde nogle besynderlige begrænsninger, f.eks. kunne man ikke lave if-else-if kæder længere end 3. Til gengæld kunne man lave arbitrære udtryk i case/when, så jeg endte med den "smukke" konstruktion i stil med:

select  
  when antal_agurker>2 then  
    ...  
  when sommertid(nu) then  
    ...  
  when moms_procent>22 then  
    ...  
  when betal=false then  
    ...  
end

Det løste problemet.

Sprogene adskiller sig på hvad de tillade i case/when udtrykket:
* enkelt-værdier
* ranges
* wildcard-matching, evt. komplekse ligesom Erlang
* arbitrære udtryk

Der er fordele og ulemper ved hvert af valgene. Man bør lære sit sprog at kende og så bruge konstruktionen på fornuftig vis.

  • 0
  • 0
Rasmus Nielsen

I C/C++ er der dog et issue med brugervenligheden - hvor man i PHP kan bruge switch på en streng, er det ikke muligt i C/C++.


Med C++-11 kan man forholdsvist enkelt omgå dette ved at definere en "constexpr" hashfunktion, som man benytter ved hhv. switch og case. Fidusen er at case-værdierne beregnes under kompileringen. Man skal dog sikre sig at der ikke opstår utilsigtede hashkollisioner, men kompileren sikrer i det mindste at hver "case" er unik.

  • 0
  • 0
Jacob Andersen

Åh ja. Jeg har tidligere kodet en del i Contiki, hvor Protothreads (http://dunkels.com/adam/pt/index.html) benyttes til at håndtere "tråde". Dette er implementeret i C preprocessoren med makroer, der benytter noget, der minder om Duff's device til at implementere en form for cooperativ multithreading. Det har altid fascineret mig, at det var muligt at implementere et multithreading bibliotek alene i med CPP makroer. :-)

  • 0
  • 0
Rasmus Kaae

Duffs Device er et af de mere eksotiske argumenter for switch/case.

Personligt bruger jeg switch/case som erstatning for 'if(){} else if {} ... else if(){}' konstruktioner. I min optik mere læsbart.

Jeg ville dog gerne udskifte 'break' med helt almindelige scopes {}.

  • 1
  • 0
Lars Gregersen

C# 7 skulle efter sigende tillade ting som dette:

object obj;  
// ...  
switch(obj) {  
  case 42:  
    // ...  
  case Color.Red:  
    // ...  
  case string s:  
    // ...  
  case Point(int x, 42) where (Y > 42):  
    // ...  
  case Point(490, 42): // fine  
    // ...  
  default:  
    // ...  
}

hvilket da ser meget sjovt ud!

  • 0
  • 0
Poul Krogh

Jeg undrer mig også over brugen af switch. Hvorfor skrive så funktionsspecific kode?

Ok, man skal erstatte noget i en streng - kan man måske lave en funktion man kan bruge igen?

F.eks:

var subscript = {"0" : "₀", "1" : "₁", "2" : "₂", "3" : "₃", "4" : "₄","5" : "₅", "6" : "₆", "7" : "₇", "8" : "₈", "9" : "₉"};  
var superscript = {"0" : "⁰", "1" : "¹", "2" : "²", "3" : "³", "4" : "⁴","5" : "⁵", "6" : "⁶", "7" : "⁷", "8" : "⁸", "9" : "⁹"};  
   
function replaceByMap( stringToReplaceIn, map)  
{  
  for(var i in map)  
  {  
    stringToReplaceIn = stringToReplaceIn.replace(i,map[i]);  
  }  
  return stringToReplaceIn;  
}  

Koden dokumenter IMHO nærmest sig selv (i sin uoptimerede form). Kan med forsigtighed bruges til andre ting (smileys, monetære symboler osv).

Mht. switch og dens syntax kommer den fra C, hvor den giver fuldt ud mening - udeladelse af breaks kan tillade kode at dele funktionalitet i switchen. Ikke videre læseligt dog og derfor ikke klædeligt at flere såkaldte højniveau sprog bare har importeret syntaxen.

  • 0
  • 0
Log ind eller Opret konto for at kommentere