14

My Software Engineering teacher just said: "Avoid using continue and break, always make it work without using these". Is there a problem with these instructions? I would say he didn't say something so coherent, but maybe I don't know something about it?

theonlygusti
  • 418
  • 2
  • 9
Shi Nha
  • 171
  • 1
  • 5
  • 16
    Does this answer your question: [Are `break` and `continue` bad programming practices?](https://softwareengineering.stackexchange.com/questions/58237/are-break-and-continue-bad-programming-practices) – Doc Brown Dec 07 '21 at 05:55
  • 3
    Did they say to _avoid_ using those instructions, or to _never_ use them? – Nat Dec 07 '21 at 07:55
  • 4
    As an example of a case where using `break` obviously makes sense: say you're looping over a collection, looking for an item. Once you've found it, presumably you'd `break` out of the loop rather than continuing to search. – Nat Dec 07 '21 at 07:58
  • 4
    Returning early from a loop is often the only reasonable thing to do, which is why these constructs exist. However, often it is much more readable to replace a `break` with `return`, i.e. to refactor an inner loop into its own small method. – Kilian Foth Dec 07 '21 at 08:20
  • @Nat you could also just have your looping condition match, if you found what you were looking for. – Kami Kaze Dec 07 '21 at 12:21
  • 1
    @KamiKaze: Yeah, that could work in some cases. But, some loop-constructs don't provide an option like that, e.g. `foreach`. – Nat Dec 07 '21 at 12:32
  • @Nat that's new, heretical witchcraft! You only need do, while and for. Everything else is redundant – Kami Kaze Dec 07 '21 at 12:36
  • `Continue` and `break` are hard to understand. Modern languages prefer the human-readable `some`, `any`, `filter`, `none` and related constructs. – knallfrosch Dec 07 '21 at 13:29
  • @KamiKaze If you wanna get super-basic, you only really need one of those three things - you can pick which one. Or you could pretend it's Assembly and just do everything with if's and goto's... – Darrel Hoffman Dec 07 '21 at 13:53
  • it depends..... – PeterK Dec 07 '21 at 16:40
  • How much experience do the students have? Is this a 1st semester programming course, or has everyone had algorithms and so on, can easily write large if/else's inside of nested loops... ? One of the answers assumes the former, but the rest seem to assume this is a 4th-year course giving real-world advice. – Owen Reynolds Dec 07 '21 at 18:02
  • 3
    I have had a lot of issues with developers being told complete nonsense by teachers. Do what you need to do to pass but programming teachers are not generally considered authoritative in software. Quite the opposite, really. Caveat emptor. – JimmyJames Dec 07 '21 at 18:51
  • This is a good example of why not to take any statement of "X is bad" or "Y is good" as meaningful without an explanation of why. – Andy Lester Dec 09 '21 at 19:28
  • These are the new `goto`. Although `break`s in a switch statement are better than `return`s. – QuentinUK Dec 21 '21 at 08:38

5 Answers5

43

Some teachers oversimplify on this topic (especially when they only teach, but don't do daily real-world programming any more). Of course, I don't know if that applies to your teachers, but I would not listen to the advice against continue and break too literally.

continue and break can make loops more readable or less readable, depending on how they are used. The real problem are loops with too large inner bodies and many conditions for stopping them or executing only parts. Having multiple continue and break in such a loop is only a symptom for this "disease", but working around those keywords just formally isn't the cure.

If running into such a situation, one could try to avoid the mentioned keywords by using boolean flags and complex if/else blocks instead, but that will not make the code simpler. Quite the opposite - often it will become even uglier.

So what is the cure?

  • Refactor inner parts of large loops into smaller functions. These functions might return some status information which can be used to control the outer calling loop, and it can be perfectly fine to use break or continue controlled by the returned status. If the functions are still complex, decompose them to smaller functions themselves.

  • Avoid processing too many things in one loop. Instead, organize your code to process sets of data, which can lead to a sequence of two or three simpler loops instead of one complex one. So there might be one loop which produces some intermediate array of data, then a second one which takes the output of the first loop, iterates over this data and hands the result set to a third. Often, each of these loops can be put into a function on their own, returning its result set, which is passed as a parameter into the next function.

You will be astonished how much much simpler your code will get when you just apply these two guidelines rigorously.

Doc Brown
  • 199,015
  • 33
  • 367
  • 565
  • Funnily enough, break and continue tend to proliferate in overly complex code. I'd say any code containing those is badly designed, since it should be trivial to write it without them in the first place. – Martin Dec 07 '21 at 12:19
  • 28
    @Martin Cars involved in traffic jams tend to have four wheels. Thus, any car having four wheels is badly designed, since it should be trivial to make one without it in the first place. – Deduplicator Dec 07 '21 at 13:21
  • 2
    @Deduplicator it's quite easy to write good code without break and continue. It's not as easy to make good cars without four wheels. – Martin Dec 07 '21 at 14:57
  • 18
    @Martin I disagree. I know many instances of code where getting rid of break or continue would be at a cost of readability. Sure, `if (!item.interesting()) continue;` can be trivially replaced by `if (item.interesting()) { ... }`, but this extra indentation is unnecessary clutter. Sure, you can extract the loop into a separate function to use return, but that means the continuity of code is broken, and it is often a much bigger hit to readability. – Frax Dec 07 '21 at 16:13
  • 5
    @Frax Many programming languages nowadays allow working with collections (lists, sequences, sets) in a functional way. In Java for example, you use `.filter(Item::isInteresting)` to get rid of those elements from your stream. C# has similar functions, as do python and various other languages. Embracing a functional style to work with data often leads to more readable code and way fewer `continue`s and `break`s. – Polygnome Dec 07 '21 at 16:22
  • 3
    @Polygnome You make a good point but I've seen people take this way too far and make their code completely arcane in an attempt to use filters and avoid control statements. Even worse, sometimes it results in performance problems or bugs. When your filter condition is simple, it's a great practice. Beyond one or two steps, filters tend to make code worse IMO. – JimmyJames Dec 07 '21 at 19:01
  • Do programming lessons start with map/filter/reduce before while or for loops nowadays? Seems like the problem with huge inner bodies should be almost nonexistent in 2021 – JollyJoker Dec 07 '21 at 20:37
  • @JollyJoker only if you learn about recursion before loops – Bergi Dec 07 '21 at 21:10
  • @bergi Why recursion? – JollyJoker Dec 07 '21 at 21:12
  • @Martin "It's not as easy to make good cars without four wheels." -- Motorcycles. They aren't as prone to jams either, but there's pros and cons to choosing between cars and motorcycles, like between choosing to use `continue`/`break` or not. – JoL Dec 07 '21 at 21:27
  • @JollyJoker Because you cannot introduce map/filter/fold without either loops or recursion – Bergi Dec 08 '21 at 00:40
  • @JollyJoker: *"the problem with huge inner bodies should be almost nonexistent in 2021"* - if you believe this, I think you should work with more legacy software from different areas ;-). – Doc Brown Dec 08 '21 at 06:38
  • @Frax are you seriously saying that moving complex loops to separate functions complicates readability? – Martin Dec 08 '21 at 07:33
  • @JoL Bikes are not cars. Moreover, I have yet to see any advantages of using break and continue. – Martin Dec 08 '21 at 07:35
  • @JimmyJames As soon as your filter becomes more complex, you make it its own method. That way it is unit-testable (which you want, anyways!) and it is back to one line in your stream-chain. Imho. stuff like `foo.stream().filter().foreachRemaining()` is *much* more readable then `for(var x : foo) if (!) continue; `. I especially like that using functional approaches automatically makes you want to split your steps in smaller functions, leading to better code and less buried traps. – Polygnome Dec 08 '21 at 09:47
  • 1
    @Polygnome At a high-level, I completely agree. The problem comes when people get religious about it. The kind of issue I've see in my work is that you might have 5 steps to process a record, each of which are heavyweight and at any step, you might find that the record should be skipped. You might be able to structure that with a filter and avoid repeating all the steps for each 'good' record but it's not nearly as straightforward or obvious as using continue. – JimmyJames Dec 08 '21 at 14:57
  • 1
    @JimmyJames "The problem comes when people get religious about it" Yes, absolutely. I have often found myself thinking the same thing. Often, good advice / guidelines are suddenly taken as unalterable gospel. So from "It is advisable to avoid X, because it often leads to Y", people *too often* go to "You can never use X, period". And that is simply not good. Have I used `continue`? You bet I have. Will I continue to use it, if it makes my code better? Sure. Will I avoid whenever it is reasonable to? Certainly. The prime measurement should be readability (leads to maintainability), not doctrine – Polygnome Dec 08 '21 at 17:54
  • @Polygnome "Will I *continue* to use it, if it makes my code better? Sure." I see what you did there... – JimmyJames Dec 08 '21 at 17:55
  • @Martin Complex problems require complex solutions. It's often better to bite the bullet and write a complex loop than to artificially break it down and pretend the complexity went away. Extracting the loop will cost you boilerplate (likely tons of boilerplate, because you need to push through all the necessary local variables), increase the cost of refactoring, and makes code non-linear, and so harder to follow. It also adds more names to track, likely contrived and non-intuitive, because you just yanked some arbitrary part out of the algorithm. – Frax Dec 08 '21 at 23:22
  • One thing to add is that it applies especially for actively developed code. You don't know yet which paths are going to survive. Maybe you will cut out half of the branches next week. Maybe you'll add 2 more. Breaking complexity down _sensibly_ is a huge investment, and it's often just not worth it. Breaking it down stupid way is going to bite you in the rear, and you may expect someone down the line shouting expletives at you, sooner or later. – Frax Dec 08 '21 at 23:36
  • @Polygnome Functional approach can be appealing, but I often find it more problematic than helping when dealing with complexity. It forces a very specific syntax, with extra indentation, often making the code cramped, and results in complex expressions. Big advantage of break/continue instead of filters is that these are separate statements. That gives a lot of space for comments. Like, actually writing what given condition means _in the specific context_. "Skip regions too small to contain a FizzBuzz", "Skip regions with ugly outline". "We can skip all records with because ". – Frax Dec 09 '21 at 00:01
  • @Frax "It forces a very specific syntax, with extra indentation, often making the code cramped, and results in complex expressions." My experience has been the exact opposite. Functional approaches help break down functions into smaller ones. This gives you the opportunity to add meaningful names and add documentation to the function, as well as their parameters. It breaks your complex algorithms down in smaller parts that can easily be unit-tested. If you have a problem with indentation in your code, your cyclomatic complexity is likely way too high, anyways. – Polygnome Dec 09 '21 at 09:14
  • @Polygnome I should've used "sometimes" instead of "often". So my point is not to the contrary of yours, rather complementary: both cases exist, FP being better or not. Local style customs play a big role, too. As for cyclomatic complexity: it's a bit overrated. Having multiple if statements in a clear sequence is rerely harder to read than if you pack them into subprocedures, yet CC differs dramatically. OTOH, higher level functions (like filter) are inherently complex, but CC doesn't capture it in any way. – Frax Dec 11 '21 at 12:21
18

You've been given some incomplete and oversimplified advice. However, that's not necessarily a bad thing on your teacher's part, as it's nearly impossible to explain the finer nuances of software development practices to newcomers straight out of the gate (without teaching at a snail's pace and boring the students to death).

What your teacher is trying to achieve here is that you learn to write iterations the proper way, without immediately starting to reach for the crutches that continue and break are. It's not that you'll never be allowed to use them as a developer, it's that you are being forbidden from using them right now as a student, specifically so that you learn to write iterations the right way.

It's the equivalent of telling a math student that they can't use a calculator, or an apprentice carpenter that they can't use a nail gun. Not because calculators and nail guns are bad, but because they are a shortcut that causes you to not learn how to do math in your head or create proper fitting wood joints.

Flater
  • 44,596
  • 8
  • 88
  • 122
  • 6
    The advice is oversimplified to the point of being wrong. A `found` variable is often way, way worse than a break. – Jan Hudec Dec 07 '21 at 16:50
  • 2
    @JanHudec: IF that is the task put before the students, yes. But breaks and continues can very easily be abused to skirt around tasks that specifically try to teach how to write iterations (e.g. a for which needs an atypical condition evaluation). The teacher is not correct about real world development but they may be correct for their specific curriculum and lesson goals. – Flater Dec 07 '21 at 17:48
4

Good programming requires prioritizing a number of principles. Two of those principles are to avoid needlessly using "break" and, especially, "continue", but those principles should be set aside when they would conflict with other more important principles.

When I was first learning to program in the 1970s, a common paradigm was:

Read a record
While last read was successful
  Process a record
  Read a record
End While

Writing the code in this way will ensure that every iteration of the loop will run to completion, which is a desirable principle to uphold, but violates another principle "Don't repeat yourself" by requiring that the instructions to read a record be included twice in the code. I think it's valuable to be able to give priority to different principles depending upon circumstances, even though I think for most purposes today a better pattern would be:

Begin Loop
  Read a record
  If read failed then
    Exit Loop
  End If
  Process a record
End Loop

Thus, for purposes of an academic exercise, writing the loop with the old pattern is useful, even though in most situations the modern approach would be more appropriate.

supercat
  • 8,335
  • 22
  • 28
  • 1
    Even in the 1970s, the DRY principle was known, of course not under that name. However, Pascal was often used for teaching, and old Pascal dialects simply did not have a break, continue, or "exit loop" equivalent. That typically let to code like in your first example, or to code which made use of boolean flags plus an if/else block inside the loop to avoid the repetition. – Doc Brown Dec 08 '21 at 14:16
  • 1
    @DocBrown: I recall seeing the first pattern taught for FORTRAN, PL/I, and HP-BASIC, none of would have imposed syntactic restrictions on loop structure. I wouldn't be at all surprised, though, if FORTRAN compilers gained the ability to optimize loops that didn't contain any internal branches before they gained the ability to optimize a wider range of constructs. A compiler may be able to recognize if, *after optimization*, the tail of a loop matches the code preceding it and replace the earlier code with a jump into the tail, more easily than it could take a common piece of source code... – supercat Dec 08 '21 at 16:28
  • ...and generate separate pieces of machine code for use before the first iteration and at the end of each subsequent iteration. – supercat Dec 08 '21 at 16:30
2

Very often the advice for a beginner and for an advanced person is different. In your case: If you ask whether you should avoid continue and break, then you should. Until you are advanced enough to make your own decision.

Important: In following the advice, don't replace continue and break with something worse. Both continue and break can be easily replaced with a goto statement (if your instructor didn't mention goto). That would be 100 times worse. Often break inside a loop can be replaced with a return statement - which is say twice as bad. Often continue or break are the natural way to express your intent, and replacing them makes your code worse.

Where it is excellent advice is when you use break or continue as a convoluted solution to your problem when something else is much easier. For example, if I saw this just at the end of a loop:

if (x == 0) continue;
y = x;

instead of the simple

if (x != 0) y = x;

then get rid of the continue.

gnasher729
  • 42,090
  • 4
  • 59
  • 119
  • 2
    IMHO your example is contrived to show only what you wanted to show. I generally find that when needing something similar, the "*y=x*" clause is a large block of code rather than a simple statement. So using the non-continue solution forces an extraneous indentation. – Peter M Dec 07 '21 at 14:06
  • Why is return 'twice as bad'? Not using early returns is highly correlated with bad code in my experience. – JimmyJames Dec 07 '21 at 18:47
  • Well, yes. break from a loop just before the end of a function, and using an early return instead, has identical effect. The early return is twice as bad as the break. – gnasher729 Dec 07 '21 at 19:03
  • 1
    @gnasher729 You've reasserted that it's twice as bad but I still don't know 'why' you think that. They don't have identical effects either. `break` causes a loop to terminate. `return` causes execution to return to the point at which the function/method was called. – JimmyJames Dec 07 '21 at 19:55
  • @JimmyJames, I think my comment was clear enough. – gnasher729 Dec 08 '21 at 23:16
  • 1
    OK but I don't. You still haven't explained why you think return is twice as bad. It's especially puzzling if you think they have identical effects. – JimmyJames Dec 09 '21 at 15:11
0

One point not already mentioned is that with break, and possibly continue, reasoning formally about/with loop invariants (also on SO) becomes more complicated.

Pablo H
  • 598
  • 2
  • 7
  • 1
    What makes you think that? You execute some code, you skip some other code, as appropriate to make an algorithm work correctly. Whether you use break or continue to skip what needs skipping doesn't make it harder to argue about it. – gnasher729 Dec 07 '21 at 19:01
  • 1
    @gnasher729 Perhaps I was too terse. Or perhaps wrong. Added 'formally' to clarify. I was thinking about precondition, loop invariant, postcondition as taught in some courses. The usual method includes "at the end of `while(condition) { ... }` you have `not condition and loop-invariant` true". If you jump out of the loop with a `break` then the loop condition may still hold, so it's more complicated. What do you think? [By the way, if the downvote is yours, thank you for explaining!] – Pablo H Dec 09 '21 at 17:22