Apparently, \foreach \x in {<a>,<b>,...,<c>}{<code>} always executes the cycle with <a> and <b>; then it sees ... and decides what the difference between <a> and <b> is. Only at this point it starts a recursion which terminates when the next number exceeds <c> (in absolute value).
If your values are only integers, you can use expl3 that also has other benefits; for instance, you use #1 instead of \x and you need no trick for expansion; moreover, no group is opened.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
% key-value form
\NewDocumentCommand{\xforeach}{mm}
{
\keys_set:nn { pasaba/xforeach }
{
start = 0,step = 1,end = 0,#1
}
\int_step_inline:nnnn
{ \l_pasaba_xforeach_start_int }
{ \l_pasaba_xforeach_step_int }
{ \l_pasaba_xforeach_end_int }
{ #2 }
}
\keys_define:nn { pasaba/xforeach }
{
start .int_set:N = \l_pasaba_xforeach_start_int,
step .int_set:N = \l_pasaba_xforeach_step_int,
end .int_set:N = \l_pasaba_xforeach_end_int,
}
% macro form
\NewDocumentCommand{\sforeach}{mmmm}
{
\int_step_inline:nnnn { #1 } { #2 } { #3 } { #4 }
}
\ExplSyntaxOff
\begin{document}
\textbf{Key-value form}
$(2,2,12)$: \xforeach{start = 2,step = 2,end = 12}{#1 }
$(2,2,4)$: \xforeach{start = 2,step = 2,end = 4}{#1 }
$(2,2,2)$: \xforeach{start = 2,step = 2,end = 2}{#1 }
$(2,2,-1)$: \xforeach{start = 2,step = 2,end = -1}{#1 }
\bigskip
\textbf{Command form}
$(2,2,12)$: \sforeach{2}{2}{12}{#1 }
$(2,2,4)$: \sforeach{2}{2}{4}{#1 }
$(2,2,2)$: \sforeach{2}{2}{2}{#1 }
$(2,2,-1)$: \sforeach{2}{2}{-1}{#1 }
\end{document}

Now, suppose you have some \foreach in a macro, say
\newcommand{\foo}[1]{%
\foreach \x in {2,4,...,#1}{do something with \x}%
}
You can turn it into a macro based on \sforeach with
\newcommand{\foo}[1]{%
\sforeach{2}{2}{#1}{do something with ##1}%
}
The parameter #1 in \sforeach must become ##1, because it is used inside a macro definition.
a,b,...,zform then I think you always get a,b and then a possibly empty iteration until the counter, stepping by b-a is at least z. – David Carlisle Nov 06 '16 at 16:56\foreachprocesses each item in the list; it decides if it is numeric and, in this case it saves the difference with the previous item, if numeric, instead of the default 1; then it goes to the next; if it is..., it goes on using the remembered difference until going beyond the next value (which should be numeric). It definitely does not parse the entire list before starting. – egreg Nov 06 '16 at 17:12pgf\foreachloop structure is meant for cases where there is something to 'do' based on a pattern: I suspect that the behaviour if you give defective input such as2,4,...,-1is undefined. – Joseph Wright Nov 06 '16 at 17:58