Modern Answer
LINQ now has a built-in Zip method, so you don't need to create your own. The resulting sequence is as long as the shortest input. Zip currently (as of .NET Core 3.0) has 2 overloads. The simpler one returns a sequence of tuples. It lets us produce some very terse code that's close to the original request:
int[] numbers = { 1, 2, 3, 4 };
string[] words = { "one", "two", "three" };
foreach (var (number, word) in numbers.Zip(words))
Console.WriteLine($"{number}, {word}");
// 1, one
// 2, two
// 3, three
Or, the same thing but without tuple unpacking:
foreach (var item in numbers.Zip(words))
Console.WriteLine($"{item.First}, {item.Second}");
The other overload (which appeared earlier than Core 3.0) takes a mapping function, giving you more control over the result. This example returns a sequence of strings, but you could return a sequence of whatever (e.g. some custom class).
var numbersAndWords = numbers.Zip(words, (number, word) => $"{number}, {word}");
foreach (string item in numbersAndWords)
Console.WriteLine(item);
If you are using LINQ on regular objects (as opposed to using it to generate SQL), you can also check out MoreLINQ, which provides several zipping methods. Each of those methods takes up to 4 input sequences, not just 2:
EquiZip
- An exception is thrown if the input sequences are of different lengths.
ZipLongest
- The resulting sequence will always be as long as the longest of input sequences where the default value of each of the shorter sequence element types is used for padding.
ZipShortest
- The resulting sequence is as short as the shortest input sequence.
See their examples and/or tests for usage. It seems MoreLINQ's zipping came before regular LINQ's, so if you're stuck with an old version of .NET, it might be a good option.