I searched a lot in the Stack/Google and didn't find a proper answer to my question, so here I go:
I know that using some calculations and stuff in for is utterly bad practice like:
for ($i = 0; $i < count($items); $i++)
Recently I saw a strange practice in the code that I'm working:
foreach (SomeModel::find() as $item)
It's ok to put the find directly in the foreach, like a count($items) or sort of? I don't like the idea, so instinctively I avoid that putting in some var, but now my task is improve/review the code and I want to put a end in my ignorance about that, someone can clarify me if it's acceptable that implementation and why?
I know that foreach is an intelligent implementation, just don't know if this structure do that kind of optimization.
In contrast to for()
, where each instruction is evaluated in each iteration, foreach()
gets the array
at the beginning, and then moves the pointer to iterate through it:
The first form loops over the array given by array_expression. On each iteration, the value of the current element is assigned to $value and the internal array pointer is advanced by one (so on the next iteration, you'll be looking at the next element).
Emphasis mine
So your function gets called just once. You can see this behavior looking at the opcodes.
<?php
function find() {
return [1, 2, 3, 4, 5, 6, 7, 8];
}
foreach (find() as $n) {
echo $n.PHP_EOL;
}
Opcodes:
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > NOP
7 1 INIT_FCALL 'find'
2 DO_FCALL 0 $1
3 > FE_RESET_R $2 $1, ->8
4 > > FE_FETCH_R $2, !0, ->8
8 5 > CONCAT ~3 !0, '%0A'
6 ECHO ~3
7 > JMP ->4
8 > FE_FREE $2
9 9 > RETURN 1
And with assignment:
<?php
function find() {
return [1, 2, 3, 4, 5, 6, 7, 8];
}
$f = find();
foreach ($f as $n) {
echo $n.PHP_EOL;
}
Opcodes:
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > NOP
6 1 INIT_FCALL 'find'
2 DO_FCALL 0 $2
3 ASSIGN !0, $2
7 4 > FE_RESET_R $4 !0, ->9
5 > > FE_FETCH_R $4, !1, ->9
8 6 > CONCAT ~5 !1, '%0A'
7 ECHO ~5
8 > JMP ->5
9 > FE_FREE $4
9 10 > RETURN 1
As you can see, the only difference is the ASSIGN
instruction, which is when the assignment to $f
is made.
So this is mostly a readability issue. In my opinion, it's a bit more readable to assign the return of the method in a variable with an appropriate name. Uncle Bob, however, states the contrary in Clean Code:
Control variables for loops should usually be declared within the loop statement, as in this cute little function from the same source.
public int countTestCases() {
int count= 0;
for (Test each : tests)
count += each.countTestCases();
return count;
}
If you have an for
loop, the condition is checked in every iteration. So instead of just comparing a var against an value, you have to call an function (count
) in every iteration. So moving the count condition before the loop makes a difference
$count = count($items);
for ($i = 0; $i < $count; ++$i) { ... }
foreach
behaves different. Instead of evaluating SomeModel::find()
in every iteration, this part is only evaluated once and saved just for the loop. You could also save the result of SomeModel::find()
in an var and use that var for the foreach
loop, but that doesn't make an difference. If you use the var only for the loop, the compiler should do exactly the same.
But keep in mind. That kind of optimization should be part of the compiler and the timing benefits should be near to 0, if you only have a few iterations.