views:

152

answers:

5

I'm wondering if there is built-in .NET functionality to change each value in an array based on the result of a provided delegate. For example, if I had an array {1,2,3} and a delegate that returns the square of each value, I would like to be able to run a method that takes the array and delegate, and returns {1,4,9}. Does anything like this exist already?

+14  A: 

LINQ provides support for projections using the Select extension method:

var numbers = new[] {1, 2, 3};
var squares = numbers.Select(i => i*i).ToArray();

You can also use the slightly less fluent Array.ConvertAll method:

var squares = Array.ConvertAll(numbers, i => i*i);

Jagged arrays can be processed by nesting the projections:

var numbers = new[] {new[] {1, 2}, new[] {3, 4}};
var squares = numbers.Select(i => i.Select(j => j*j).ToArray()).ToArray();

Multidimensional arrays are a little more complex. I've written the following extension method which projects every element in a multidimensional array no matter what its rank.

static Array ConvertAll<TSource, TResult>(this Array source,
                                          Converter<TSource, TResult> projection)
{
    if (!typeof (TSource).IsAssignableFrom(source.GetType().GetElementType()))
    {
        throw new ArgumentException();
    }
    var dims = Enumerable.Range(0, source.Rank)
        .Select(dim => new {lower = source.GetLowerBound(dim),
                            upper = source.GetUpperBound(dim)});
    var result = Array.CreateInstance(typeof (TResult),
        dims.Select(dim => 1 + dim.upper - dim.lower).ToArray(),
        dims.Select(dim => dim.lower).ToArray());
    var indices = dims
        .Select(dim => Enumerable.Range(dim.lower, 1 + dim.upper - dim.lower))
        .Aggregate(
            (IEnumerable<IEnumerable<int>>) null,
            (total, current) => total != null
                ? total.SelectMany(
                    item => current,
                    (existing, item) => existing.Concat(new[] {item}))
                : current.Select(item => (IEnumerable<int>) new[] {item}))
        .Select(index => index.ToArray());
    foreach (var index in indices)
    {
        var value = (TSource) source.GetValue(index);
        result.SetValue(projection(value), index);
    }
    return result;
}

The above method can be tested with an array of rank 3 as follows:

var source = new int[2,3,4];

for (var i = source.GetLowerBound(0); i <= source.GetUpperBound(0); i++)
    for (var j = source.GetLowerBound(1); j <= source.GetUpperBound(1); j++)
        for (var k = source.GetLowerBound(2); k <= source.GetUpperBound(2); k++)
            source[i, j, k] = i*100 + j*10 + k;

var result = (int[,,]) source.ConvertAll<int, int>(i => i*i);

for (var i = source.GetLowerBound(0); i <= source.GetUpperBound(0); i++)
    for (var j = source.GetLowerBound(1); j <= source.GetUpperBound(1); j++)
        for (var k = source.GetLowerBound(2); k <= source.GetUpperBound(2); k++)
        {
            var value = source[i, j, k];
            Debug.Assert(result[i, j, k] == value*value);
        }
Nathan Baulch
@Nathan Baulch: Your example should probably use a delegate rather than to specify the function in the lambda, to answer the question more specifically.
Scott Stafford
Very neat. Will this take into account multi-dimensional arrays?
JustOnePixel
Ah there it is. I was looking for an Apply method or something. Never would have thought of it being named Select, but I suppose that goes along with the LINQ basis of all the fun extension methods.
jdmichal
+4  A: 

Using System.Linq you could do something like:

var newArray = arr.Select(x => myMethod(x)).ToArray();
statichippo
+2  A: 

LINQ queries could easily solve this for you - make sure you're referencing System.Core.dll and have a

using System.Linq;

statement. For example, if you had your array in a variable named numberArray, the following code would give you exactly what you're looking for:

 var squares = numberArray.Select(n => n * n).ToArray();

The final "ToArray" call is only needed if you actually need an array, and not an IEnumerable<int>.

Ben
Awesome, thanks. Will this take into account multi-dimensional arrays?
JustOnePixel
No - for that, you'd want a nested select, like numberArrays.Select(as => as.Select(n => n * n).ToArray()).ToArray(). For an arguably more readable approach, just use two nested loops.
Ben
+6  A: 

Not that I'm aware of (replacing each element rather than converting to a new array or sequence), but it's incredibly easy to write:

public static void ConvertInPlace<T>(this IList<T> source, Func<T, T> projection)
{
    for (int i = 0; i < source.Count; i++)
    {
        source[i] = projection(source[i]);
    }
}

Use:

int[] values = { 1, 2, 3 };
values.ConvertInPlace(x => x * x);

Of course if you don't really need to change the existing array, the other answers posted using Select would be more functional. Or the existing ConvertAll method from .NET 2:

int[] values = { 1, 2, 3 };
values = Array.ConvertAll(values, x => x * x);

This is all assuming a single-dimensional array. If you want to include rectangular arrays, it gets trickier, particularly if you want to avoid boxing.

Jon Skeet
+1 for ConvertAll which actually is what the OP asked for "method that takes the array and delegate, and returns..."
Conrad Frix
I don't get why this beats Nathan's answer for the accepted answer. What am I missing? Select does exactly what he needs, no?
Richard Hein
@Richard: It's possible that Nathan's answer was as complete when my answer was accepted, in terms of the multi-dimensional support. The way I read the OP's question to start with, I thought he wanted to modify the array in place, which only my answer covers. For array to array conversion, `Array.ConvertAll` is more efficient and doesn't require .NET 3.5. If only a sequence is required, `Select` is fine as mentioned in my answer.
Jon Skeet
A: 

you can use linq to accomplish this in shorthand but be careful remember that a foreach occurs underneath anyway.

int[] x =  {1,2,3};
x = x.Select(( Y ) => { return Y * Y; }).ToArray();
rerun