As others have mentioned, C # does not give you a guarantee that a tail recursion will be optimized. I'm very fan of tail recursion but I think you're focusing a bit in the wrong direction - I think flexible flow control is a more crucial point of tail recursion than immutability and this changes somewhat the way address this problem.
In a simple loop case with your example, using tail recurs ends up being as mutable and low level as writing your code using gotos. In each step you have to update the accumulator and counter and to say that you will jump back to the beginning of the loop.
int acc = 1;
int i = N;
loop:
if (i >= 0) {
// finge que pode usar atribuição múltipla estilo Python
i, acc = i-1, acc*i; goto loop;
} else {
return acc;
}
The only advantage of tail recursion compared to gotos is the assignment of more than one variable in one step and the compiler will warn you if you forget to tell the new value to one of the variables. Anyway, a for loop ends up being more structured and high level, since you only have to take care of the logic to update the product and the updating of the counter and the gotos comes for free. It's a bit similar to programming using a fold instead of tail recursion
int acc = 1;
for (int i = N; i >= 0; i--){
acc *= i;
}
Only tail recursion is not just for making simple loops in which a function calls itself. The part where tail recursion makes the most difference is when you have more than one mutually recursive function. A forced example is this state machine defined in Haskell:
par 0 = True
par n = impar (n-1)
impar 0 = False
impar m = par (m-1)
The equivalent of this without recursion is a state machine:
int n;
int m;
par:
if (n == 0) {
return true;
} else {
m = n - 1;
goto impar;
}
impar:
if (n == 0) {
return false;
} else {
n = m - 1;
goto par;
}
However in this release all functions have to be combined in a single code snippet, which hurts the encapsulation. For example, we need to declare all the parameter variables at the top, which makes it possible to use them in the wrong place.
In addition, tail recursion is also present when we call a function that was passed to us as a parameter. This is equivalent to a computed goto and can not be translated into a static loop.
In these last two cases, the language support for tail recursion is most needed. If you have to do something equivalent, the closest will be to use the standard "trampoline", but it is kind of inefficient and well chatinho to use.