Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[DE] Fixing the Billion Dollar Mistake (C# Brille)

Richard
February 08, 2022

[DE] Fixing the Billion Dollar Mistake (C# Brille)

Sir Anthony Hoare bezeichnet die Null-Referenz als seinen Milliarden-Dollar Fehler, weil sie zu "unzähligen Fehlern, Schwachstellen und Systemabstürzen geführt hat, die in den letzten vierzig Jahren wahrscheinlich eine Milliarde Dollar an Schmerz und Schaden verursacht hat."

Ob die Größenordnung stimmt, muss die Wissenschaft erst noch beweisen, aber ich denke, man kann mit Fug und Recht behaupten, dass Null- Referenzen eine der Hauptursachen für Schlafentzug bei Entwicklern sind, die kurz vor dem Release stehen. Glücklicherweise wurden die meisten Programmiersprachen um alternative Möglichkeiten erweitert, mit der "absence of value" umzugehen, was zu aussagekräftigerem Code, weniger Raum für Fehler und besserem Schlaf führt.

In diesem Vortrag werden wir die Nullability-Möglichkeiten erkunden, die C# 8 und 9 bieten. Dies vergleichen wir mit den Wege, die andere Sprachen wie Python, Kotlin oder F# gewählt haben. Und werfen auch einen Blick auf Techniken wie Railway-Oriented Programming oder das Null-Object-Pattern, die helfen können, wenn die Sprache der Wahl keine Option bietet.

Gehalten bei der JUG-Hamburg: https://www.meetup.com/de-DE/jug-hamburg/events/283401850/

Richard

February 08, 2022
Tweet

More Decks by Richard

Other Decks in Programming

Transcript

  1. Folien von @arghrich Fixing the Billion Dollar Mistake (C# Brille)

    08.02.22 richargh.de/ speakerdeck.com/richargh Richard Gross (er/ihm) Arbeitet bei maibornwolff.de/
  2. Folien von @arghrich 2021 CWE Top 20 Most Dangerous* Software

    Weaknesses 7 *„most common and impactful issues experienced over the previous two calendar years.” https://cwe.mitre.org/top25/archive/2021/2021_cwe_top25.html & Comment by Jeff Atwood #15 Null Pointer Dereference Input Validation › ‚Cross-Site Scripting‘ › ‚OS Command Injection‘ Auth › Missing Authentication / Authorization Manual Memory › Out-of-bounds read/write #12 Int Overflow / Wraparound 1. // Java binary Search bug 2. binarySearch(int[] a, int key){ 3. int low = 0; 4. int high = a.length - 1; 5. 6. while (low <= high) { 7. // Bug in JDK for 20 years 8. int mid = (low + high) / 2; 9. int midVal = a[mid]; 10. // …
  3. Folien von @arghrich Nur 10 Exceptions sind verantwortlich für 97%

    aller geloggten Fehler 10 https://www.overops.com/blog/we-crunched-1-billion-java-logged-errors-heres-what-causes-97-of-them-2/ https://www.overops.com/blog/the-top-10-exceptions-types-in-production-java-applications-based-on-1b-events/ 97% 3% Logged Errors 10 Unique Errors Other Errors 97% der geloggten Exceptions nach Häufigkeit 1. NullPointerException 2. NumberFormatException 3. IllegalArgumentException 4. …
  4. Folien von @arghrich 11 I call it my billion-dollar mistake.

    It was the invention of the null reference in 1965 [ALGOL]. […] This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. Tony Hoare Null References: The Billion Dollar Mistake 2009 InnoQ Hat aber nicht C#, Java, oder Python designt. J
  5. Folien von @arghrich Ein Dictionary designen 1. public class Addresses

    2. { 3. private readonly Dictionary<PersonId, Address> _addresses = new(); 4. 5. public Address Find(PersonId personId) 6. { 7. return _addresses[personId]; 8. } 9. // … 14 Wenden wir das Principle of Least Surprise an: Was sollte zurückgegeben werden, wenn die personId unbekannt ist?
  6. Folien von @arghrich “Nix” modellieren 15 1. public class Addresses

    2. { 3. private readonly Dictionary<PersonId, Address> _addresses = new(); 4. 5. public Address Find(PersonId personId) 6. { 7. return _addresses[personId]; 8. } 9. // … Implizit Exception C#, Python throw KeyNotFoundException Implizit return null; Java return null; Implizit return “default” C# (bei structs) return Address.NULL; return default; Explizit Verpf. Standardwert return GetOrDefault( personId, defaultAddress); Explizit “Nix” modellieren Scala, Kotlin, F#, … return ???;
  7. Folien von @arghrich Explizites nichts 1. const address = addresses.Find(personId);

    16 2. // kann nicht ignoriert werden 3. log(`Street is ${address.Street}`); 4. // Nur zugreifbar nach Check 5. if(address != null) 6. log(`${address.Street}`) Compile error Expliziter check notwendig
  8. Folien von @arghrich 19 Sicherere Sprachen durch Wegfall von Optionen

    „Goto considered harmful“ * If, else for While return Manual memory considered harmful Garbage Collection, Reference Counting Ownership (Rust J) Implicit null considered harmful Optional<T> Maybe T Nullable? *Edsger Dijkstra „A Case Against the Goto Statement“ aka Goto Statement Considered Harmful.
  9. Folien von @arghrich F# 2005 F# empowers everyone to write

    succinct, robust and performant code Hat null aber man kann es nicht mehr zuweisen. 20 Rust 2010 A language empowering everyone to build reliable and efficient software. Besitzt kein null. Kotlin 2011 Modern, concise and safe programming language /* Get rid of those pesky NullPointerExceptions, you know, The Billion Dollar Mistake*/ Swift 2014 write software that is incredibly fast and safe by design. Besitzt nil für Objective-C interop. Man erkennt einen gewissen Trend https://en.wikipedia.org/wiki/Void_safety
  10. Folien von @arghrich Jeder versucht, mit Null umzugehen 22 Python

    Java C# https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history ‘94 ‘96 ‘02 ‘05 ‘14 ‘15 ‘19 ‘20 ‘22 C# 02 › Generics › Nullable Value Types › Null-coalescing operator ?? C# 06 › Null-propagator ?. and ?[] C# 08 .NET Core 3+ › Nullable reference types C# 09 .NET 5+ › Generic nullable improvements › Records › Init-only setters Java 8 › Optional<T> instead of nullable Python 3.5 › Type hints › Optional[T]
  11. Folien von @arghrich Finde den Fehler 24 public string FindNotebookMaker(EmployeeId

    id) { var employee = _company.FindById(id); if (employee is null) return "Employee does not exist"; /* ... */ return employee.Notebook.Maker; }
  12. Folien von @arghrich Warum ist das ein Fehler? public string

    FindNotebookMaker(EmployeeId id) { var employee = _company.FindById(id); if (employee is null) return "Employee does not exist"; /* ... */ return employee.Notebook.Maker; } 25 Aber, aber, aber, ein Employee hat immer ein notebook!!1! Ich hab das selbst so programmiert. System.NullReferenceException at FindNotebookMaker(EmployeeId id) in UsesLegacyCode.cs:line 22
  13. Folien von @arghrich Der eigentliche Fehler ist wo anders public

    void StartRepairNotebook(EmployeeId id) { var employee = _company.FindById(id); if (employee is null) return; employee.Notebook = null; _company.Put(employee); } 26 Unter bestimmten Umständen (z. B. wenn das Notebook repariert wird), hat ein Mitarbeiter kein Notebook. Dieses feature wurde erst viel später hinzugefügt oder vielleicht hast du das auch nur vergessen. Der Typ von Employee hat das nicht reflektiert. public class Employee { public EmployeeId Id {get;} public string Name {get;} public Notebook Notebook {get; set;} /*…*/ }
  14. Folien von @arghrich Nullable reference types aktivieren 1. // per

    *.csproj file: 2. <Project Sdk="Microsoft.NET.Sdk"> 3. <PropertyGroup> 4. <Nullable> 5. enable 6. </Nullable> 7. <LangVersion> 8. 8.0 9. </LangVersion> 10. <WarningsAsErrors> 11. nullable 12. </WarningsAsErrors> 13. <!– if you migrate, better start without WarningsAsErrors --> 14. </PropertyGroup> 28 https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-reference-types 1. // per *.cs file: 2. #nullable enable 3. namespace <yournamespace> 4. { 5. public class <yourclass> 6. { 7. // can now signal null 8. static string? Name() 9. { /* … */} 10. static <otherclass>? Data() 11. { /* … */} 12.// disable wherever you want 13.#nullable disable OR
  15. Folien von @arghrich Nach Enable (1) public void StartRepairNotebook(EmployeeId id)

    { var employee = _company.FindById(id); if (employee is null) return; employee.Notebook = null; _company.Put(employee); } 29 Compile error
  16. Folien von @arghrich (2) Explizite nullability Modellierung 30 public class

    Employee { public EmployeeId Id {get;} public string Name {get;} public Notebook? Notebook {get; set;} /*…*/ } Markieren als nullable
  17. Folien von @arghrich (3) Kaskadierende Compile Fehler 31 Compile Fehler

    public string FindNotebookMaker(EmployeeId id) { var employee = _company.FindById(id); if (employee is null) return "Employee does not exist"; /* ... */ return employee.Notebook.Maker; }
  18. Folien von @arghrich Wir haben die Ursache behoben, nicht nur

    ein Symptom 33 public string FindNotebookMaker(EmployeeId id) { var employee = _company.FindById(id); if (employee is null) return "Employee does not exist"; /* ... */ return employee.Notebook?.Maker; } public class Employee { public EmployeeId Id {get;} public string Name {get;} public Notebook? Notebook {get; set;} /*…*/ } public void StartRepairNotebook(EmployeeId id) { var employee = _company.FindById(id); if (employee is null) return; employee.Notebook = null; _company.Put(employee); }
  19. Folien von @arghrich Nullable Reference Types helfen uns Bugs an

    der Wurzel zu beheben 34 Slides by @arghrich
  20. Folien von @arghrich Recap: “Nix” modellieren 37 1. public class

    Addresses 2. { 3. private readonly Dictionary<PersonId, Address> _addresses = new(); 4. 5. public Address Find(PersonId personId) 6. { 7. return _addresses[personId]; 8. } 9. // … Implizit Exception C#, Python throw KeyNotFoundException Implizit return null; Java return null; Implizit return “default” C# (bei structs) return Address.NULL; return default; Explizit Verpf. Standardwert return GetOrDefault( personId, defaultAddress); Explizit “Nix” modellieren Scala, Kotlin, F#, … return ???;
  21. Folien von @arghrich Modellieren von “nix” in C#8 38 1.

    public class Addresses 2. { 3. private readonly Dictionary<PersonId, Address> _addresses = new(); 4. 5. public Address? FindAddress(PersonId personId) 6. { 7. return _addresses.GetValueOrDefault(personId); 8. } 9. // … Explizit “Nix” modellieren Scala, Kotlin, F#, C#
  22. Folien von @arghrich Umgang mit “nix” in C#8 39 1.

    // schlecht, kompiliert nicht 2. string? address = FindAddress(personId).Street; 3. // gut, benutzt null-propagating operator 4. string? address = FindAddress(personId)?.Street; Explizit “Nix” modellieren Scala, Kotlin, F#, C# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/null-checking-preferences 5. // vielleicht besser, benutzt null-coalescing operator 6. string address = FindAddress(personId)?.Street ?? "Konstitucijos Av. 20”; 7. // vielleicht noch besser, nutzt null-coalescing assignment operator 8. string? address = FindAddress(personId)?.Street; 9. address ??= "Konstitucijos Av. 20”; 10.// gut, benutzt pattern matching is null check 11.if(address is null) {/*…*/} 12.if(address is not null) {/*…*/}
  23. Folien von @arghrich 43 #4 Immer angeben, wenn null zurückgegeben

    wird 1. Address? FindAddress(PersonId pId) 2. { 3. return _addresses.GetValueOrDefault(pId); 4. } 1
  24. Folien von @arghrich 44 #5 Leere collections oder arrays zurückgeben,

    niemals null 1. // not like this 2. IList<Address>? RecentAddresses() 3. { 4. return _employee.TrackingAllowed 5. ? _recentAddresses 6. : null; 7. } 1. // like this 2. IList<Address> RecentAddresses() 3. { 4. return _employee.TrackingAllowed 5. ? _recentAddresses 6. : new List<Address>(); 7. } Effective Java 3rd Edition Item 54 Erschwert die Nutzung der Api für alle Aufrufer. 2
  25. Folien von @arghrich 45 #5 C#9 records für domain types

    nutzen 3 1. public class Address 2. { 3. public string City {get; init;} 4. public string? Street {get; init;} 5. // kompiliert nicht; der Compiler kann nicht wissen, dass City tatsächlich gesetzt wurde 6. } 1. public record Address( 2. string City, 3. string? Street 4. ); 1. // so wird ein Record initialisiert: 2. var address = new Address( 3. City: “Trakai”, 4. Street: null 5. )
  26. Folien von @arghrich C#9 records bieten value-equality und non-destruktive Mutation

    1. public record Address( 2. string City, 3. string? Street 4. ); 46 5. // value-equality 6. var address1 = new Address( 7. City: “Vilnius”, 8. Street: "Konstitucijos Av. 20" 9. ); 10. var address2 = new Address( 11. City: “Vilnius”, 12. Street: "Konstitucijos Av. 20" 13. ); 14. 15. address1 == address2 // true https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record 16.// non-destruktive Mutation 17. var address3 = address2 with { 18. Street = “Ozo g. 18” 19. } 20. address2 == address3 // false
  27. Folien von @arghrich 47 #5 Vertraue nicht auf die Non-Null-

    Signatur am Rande deines Systems 4 1. public record EmployeeDto( 2. string Id, 3. string Name, 4. string? Email) 1. var json = @"{ ""foo"": ""bar"” }"; 2. var result = JsonConvert.DeserializeObject<EmployeeDto?>(json); 3. // result = EmployeeDto { Id=, Name=, Email= } 4. result.Id is null // true was unmöglich sein sollte
  28. Folien von @arghrich 50 Ich habe nullable reference types aktiviert

    aber jetzt habe ich null-checks überall!!11!elf
  29. Folien von @arghrich 1. public IResponse CreateEmployee(Request request) 2. {

    3. var employee = EmployeeFromBody(request.Path); 4. CheckEmailIsUnique(employee); 5. StoreEmployee(employee); 6. EmailEmployee(employee); 7. return new OkResponse(200); 8. } 52
  30. Folien von @arghrich 1. public IResponse CreateEmployee(Request request) 2. {

    3. var employee = EmployeeFromBody(request.Path); 4. if(employee is null) 5. { 6. return new BadResponse(400, “Employee invalid”); 7. } 8. if(EmailIsNotUnique(employee)) 9. { 10. return new BadResponse(400, “Email must be unique”); 11. } 12. StoreEmployee(employee); 13. try { 14. EmailEmployee(employee); 15. } 16. catch(ServerUnreachableException) 17. { 18. return new BadResponse(500, “could not send email”); 19. } 20. return new OkResponse(200); 21.} 53 Wo ist mein happy path hin? Kurzes Beispiel mit nur einem null-check
  31. Folien von @arghrich Ein seichter Einstieg in das Thema Railway-Oriented

    Programming* 55 *By Scott Wlaschin https://fsharpforfunandprofit.com/rop/
  32. Folien von @arghrich Happy Path ohne error handling 56 1.

    public IResponse CreateEmployee(Request request) 2. { 3. var employee = EmployeeFromBody(request.Path); 4. CheckEmailIsUnique(employee); 5. StoreEmployee(employee); 6. EmailEmployee(employee); 7. return new OkResponse(200); 8. }
  33. Folien von @arghrich ROP Path mit error handling 57 1.

    public IResponse CreateEmployee(Request request) 2. { 3. EmployeeFromBody(request.Path).AsResult(ifnull: “invalid employee passed”) 4. .ThenTry(CheckEmailIsUnique) 5. .Then(StoreEmployee) 6. .ThenTry(EmailEmployee) 7. .Then(_ => new OkResponse(200)); 8. } Ich kann die business logic wieder sehen
  34. Folien von @arghrich Fokus auf den Happy Path, Result macht

    den Rest 58 J L thenTry then thenTryFix thenFix 1. Result<TOut> Then<TIn, TOut>( 2. Func<TIn, TOut> onOk){ /*…*/ } 3. Result<TOut> ThenTry<TIn, TOut>( 4. Func<TIn, Result<Tout>> onOk){ /*…*/ }
  35. Folien von @arghrich How to get on the rail 59

    J L Ok(sth) Fail(“why”) OfResult(sth?)
  36. Folien von @arghrich Against Railway-Oriented Programming* 60 *Also Scott Wlaschin

    https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/ https://eiriktsarpalis.wordpress.com/2017/02/19/youre-better-off-using-exceptions/
  37. Folien von @arghrich Don‘t wrap all exceptions with Results* 61

    *Also Scott Wlaschin https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/ https://eiriktsarpalis.wordpress.com/2017/02/19/youre-better-off-using-exceptions/
  38. Folien von @arghrich Use ROP only for Domain Errors* 62

    *Also Scott Wlaschin https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/ https://eiriktsarpalis.wordpress.com/2017/02/19/youre-better-off-using-exceptions/
  39. Folien von @arghrich How to get started? 63 Railway Oriented

    Programming [Original, Scott Wlaschin 2014] Against Railway-Oriented Programming [Scott Wlaschin 2019] Railway Oriented Programming: C# Edition [Tama Waddell 2019] F# nutzen J Oder Copy/Code eine eigene Result class Deep-dive talk anschauen J
  40. Folien von @arghrich Beispiele liegen auf Github • https://github.com/Richargh/fixing-the-billion-dollar-mistake •

    Beinhalten Beispiele in C#, F#, Java, Python, Scala • Beinhalten Railway-Oriented Programming Beispiele in C# • Beinhalten eine Optional-Variation von ROP in Java 65
  41. Folien von @arghrich Ich bin nicht der erste, der über

    nullable reference types spricht :) • Null References: The Billion Dollar Mistake [Tony Hoare 2009] • Nullable Reference Types in C# 8 • [Jon Skeet GOTO 2019] 66
  42. Folien von @arghrich Ich bin nicht der erste, der über

    ROP spricht :) • Railway Oriented Programming [Original, Scott Wlaschin 2014] • Against Railway-Oriented Programming [Scott Wlaschin 2019] • Railway Oriented Programming: C# Edition [Tama Waddell 2019] • Railway-Oriented Programming in C# [Marcus Denny 2017] • Railway-oriented programming in Java [Tom Johnson 2021] 67
  43. Folien von @arghrich Done richargh.de/ speakerdeck.com/richargh Code at https://github.com/Richargh/fixing-the-billion-dollar-mistake Small

    Icons by Fontawesome Richard Gross pronoun.is/he Arbeitet bei maibornwolff.de/en Folien von @arghrich
  44. Folien von @arghrich 2021 CWE Top 20 Most Dangerous Software

    Weaknesses 1. Out-of-bounds Write 2. ‘Cross-site Scripting’ 3. Out-of-bounds Read 4. Improper Input Validation 5. ‘OS Command Injection’ 6. ‘SQL Injection’ 7. Use After Free 8. ‘Path Traversal’ 9. Cross-Site Request Forgery 10. Unrestricted File Upload with ‘Evil’ Type 11. Missing Auth. for Critical Function 12. Integer Overflow or Wraparound 13. Deserialization of Untrusted Data 14. Improper Authentication 15. NULL Pointer Dereference 16. Use of Hard-Coded Credentials 17. Improper Restriction … of Memory Bounds 18. Missing Authorizations 19. Incorrect Default Permissions 20. Exposure of Sensitive Information … 70 https://cwe.mitre.org/top25/archive/2021/2021_cwe_top25.html & Comment by Jeff Atwood Rough categories Manual Memory Input Validation Auth
  45. Folien von @arghrich UseCase: Change Employee Address 71 EmployeeId From

    Request Path Find Employee Address from Path Change Address Store changed Employee Greet Employee with new Email
  46. Folien von @arghrich 1. public IResponse ChangeAddress(Request request) 2. {

    3. var employeeId = EmployeeIdFromPath(request.Path); 4. var employee = _employees.FindById(employeeId); 5. var address = AddressFromBody(request.Body); 6. employee = employee.ChangeAddress(address); 7. _employees.Store(employee); 8. GreetEmployee(employee.Email); 9. return new OkResponse(200); 10.} 72 What about DbExceptions? Does not compile. All nullable.
  47. Folien von @arghrich 1. public IResponse ChangeAddress(Request request) 2. {

    3. var employeeId = EmployeeIdFromPath(request.Path); 4. if(employeeId is null) 5. { 6. return new BadResponse(400, “Employee Id invalid”); 7. } 8. var employee = _employees.FindById(employeeId); 9. if(employee is null) 10. { 11. return new BadResponse(400, “Employee not found”); 12. } 13. var address = AddressFromBody(request.Body); 14. if(address is null) 15. { 16. return new BadResponse(400, “address invalid”); 17. } 18. employee = employee.ChangeAddress(address); 19. try { 20. _employees.Store(employee); 21. } 22. catch(MyDbException ex) 23. { 24. return new BadResponse(500, “could not change email”); 73 Where did my happy path go?
  48. Folien von @arghrich ROP Path with error handling 74 1.

    public IResponse ChangeAddress(Request request) 2. { 3. EmployeeIdFromPath(request.Path) 4. .ThenTry(_employees.FindById) 5. .ThenTryToPair(_ => AddressFromBody(request.Body)) 6. .ThenTry((employee, address) => employee.ChangeAddress(address)) 7. .ThenTry(_employees.Store) 8. .ThenTry(employee => GreetEmployee(employee.Email)) 9. .Then(_ => new OkResponse(200)); 10.} I can see my business logic again
  49. Folien von @arghrich UseCase: Rent NotebookType 75 Employee doesn’t have

    a notebook Employee has enough budget? Notebook with desired type is available? Rent notebook for employee
  50. Folien von @arghrich 1. public IResponse Rent(NotebookType type, EmployeeId eId)

    2. { 3. var employee = _employees.FindById(eId); 4. if (AlreadyHasANotebook(employee)) 5. { 6. return Bad(”Already has a notebook); 7. } 8. var notebook = _inventory 9. .FindNotebooksByType(type) 10. .FirstOrDefault(IsAvailable); 11. 12. var budget = _budget.FindById(eId); 13. if (HasNotEnoughBudget(budget, notebook)) 14. { 15. return Bad(”Not enough budget”); 16. } 17. var remainingBudget = RentNotebook( employee, budget, notebook); 18. NotifyOfRent( employee, notebook, remainingBudget); 19. return Ok(notebook); 20.} 76 Employee doesn’t have a notebook Employee has enough budget? Notebook with desired type is available? Rent notebook for employee Notify Employee Does not compile What about DbExceptions? Negative framing
  51. Folien von @arghrich 1. public IResponse Rent(NotebookType type, EmployeeId eId)

    2. { 3. var employee = _employees.FindById(eId); 4. if (employee is null) 5. { 6. return Bad("Employee does not exist); 7. } 8. if (AlreadyHasANotebook(employee)) 9. { 10. return Bad(”Already has a notebook); 11. } 12. var notebook = _inventory 13. .FindNotebooksByType(type) 14. .FirstOrDefault(IsAvailable); 15. if (notebook is null) 16. { 17. return Bad("Notebook does not exist); 18. } 19. var budget = _budget.FindById(eId); 20. if (budget is null) 21. { 22. return Bad("Employee has no budget); 23. } 24. 25. if (HasNotEnoughBudget(budget, notebook)) 26. { 27. return Bad(”Not enough budget”); 28. } 29. 30. try 31. { 32. var remainingBudget = RentNotebook( employee, budget, notebook); 33. NotifyOfRent( employee, notebook, remainingBudget); 34. return Ok(notebook); 35. } 36. catch (MyDbException e) 37. { 38. Console.WriteLine(e); 39. return Bad("Could not notebook"); 40. } 41.} 77 Where did all my business rules go?
  52. Folien von @arghrich 78 Goto considered very harmful, so please

    never use this in C# 1. [Fact(DisplayName = "We should never ever use the goto statement, it is very harmful, but note that in C# you can")] 2. void ComplexGotoExample() 3. { 4. _output.WriteLine("I'd wager this is hard to understand"); 5. var isDone = false; 6. var repeatCount = 0; 7. 8. start: 9. if (isDone) 10. goto end; 11. _output.WriteLine("First"); 12. 13. 14. 15. 16. repeat: 17. if(repeatCount > 3) 18. { 19. isDone = true; 20. goto start; 21. } 22. _output.WriteLine("Repeat"); 23. repeatCount++; 24. goto repeat; 25. 26. end: 27. _output.WriteLine("End"); 28.}