I agree, the temporary tuples are a code smell. They carry a performance overhead, but worse, they obscure the purpose of the code. Here is a way of achieving the goal by working directly on the enumerators.
private static void TripleForEach<TFirst, TSecond, TThird>(
IEnumerable<TFirst> first,
IEnumerable<TSecond> second,
IEnumerable<TThird> third,
Action<TFirst, TSecond, TThird> action)
{
using (IEnumerator<TFirst> e1 = first.GetEnumerator())
using (IEnumerator<TSecond> e2 = second.GetEnumerator())
using (IEnumerator<TThird> e3 = third.GetEnumerator())
{
while (e1.MoveNext() && e2.MoveNext() && e3.MoveNext())
{
action(e1.Current, e2.Current, e3.Current);
}
}
}