14

I have a string with several fields separated by a specific character, something like this:

A,B,C

I want to split the string at the commas and assign each resulting field to its own string variable. In Perl I can do that elegantly like this:

my ($varA, $varB, $varC) = split (/,/, $string);

What is the simplest and most elegant way to achieve the same result in C#?

I know that I can split into an array:

string[] results = string.Split(',');

But then I would have to access the fields via their index, e.g. results[2]. That is difficult to read and error-prone - consider not having 3 buth 30 fields. For that reason I prefer having each field value in its own named variable.

Helge Klein
  • 8,829
  • 8
  • 51
  • 71

8 Answers8

9

I agree. Hiding the split in an Adapter class seems like a good approach and communicates your intent rather well:

public class MySplitter
{
     public MySplitter(string split)
     {
         var results = string.Split(',');
         NamedPartA = results[0];
         NamedpartB = results[1];
     }

     public string NamedPartA { get; private set; }
     public string NamedPartB { get; private set; }
}
Ritch Melton
  • 11,498
  • 4
  • 41
  • 54
  • 1
    If I understand your answer correctly, it requires one class per usage scenario of Split because the number of results are fixed as well as the names. I want the names to be flexible. – Helge Klein Mar 10 '11 at 21:43
  • I'd say it requires one instance per type that you want to have names for. It's a minor bit of code that makes the consumer code easier to read and interpret. – Ritch Melton Mar 10 '11 at 21:48
  • Except that I'd expect `MySplitter` to be something that stores the result named parts. A splitter should just split and return the split parts. Instead, I might consider something more semantic: `Person` that has `First` and `Last` and a static method called `parse` that returns a new instance of `Person`. `person.First` would make more sense to me than `personSplitter.First` – Brian Genisio Oct 14 '11 at 13:11
7

You can use Tuples (added in .Net 4). Tuples in MSDN

This:

public class MySplitter
{
     public MySplitter(string split)
     {
         var results = split.Split(',');
         NamedPartA = results[0];
         NamedpartB = results[1];
     }

     public string NamedPartA { get; private set; }
     public string NamedPartB { get; private set; }
}

Could be achieved with something like this:

public Tuple<string,string> SplitIntoVars(string toSplit)
{
   string[] split = toSplit.Split(',');
   return Tuple.Create(split[0],split[1]);
}

With a Tuple you can use:

var x = SplitIntoVars(arr);
// you can access the items like this:    x.Item1 or x.Item2 (and x.Item3 etc.)

You can also create a Tuple for using Tuple<string,int> etc.

Also... I don't really like out parameters, so you emulate returning multiple values using a Tuple (and obviously, also of varying types). this:

public void SplitIntoVariables(string input, out a, out b, out c)
{
    string pieces[] = input.Split(',');
    a = pieces[0];
    b = pieces[1];
    c = pieces[2];
}

turns into this:

public Tuple<string,string,string> SplitIntoVariables(string[] input)
    {
        string pieces[] = input.Split(',');
        return Tuple.Create(pieces[0],pieces[1],pieces[2]);
    }

Other (more imaginative) options could be creating an ExpandoObject (dynamic) that holds your values (something akin to ViewBag in ASP.NET MVC)

Linkgoron
  • 4,866
  • 2
  • 26
  • 26
  • 1
    Interesting. Did not know about tuples before. – Helge Klein Mar 10 '11 at 22:09
  • One of the reason for not suggesting a Tuple is the fact that result[0] isn't much clearer than tuple.Item1, Item2, etc.... The OP explicitly stated a desire for named values. – Ritch Melton Mar 11 '11 at 02:01
  • I think it's still better than arrays, as it's not exactly using indexes, and you don't recreate structures that exist in the FW. Either way, I think the best solution would probably involve a solution that's closer to the actual problem, and not something so general as the current question. IMO the most elegant solution (if you cant create a good concrete class) would be something such as a Dynamic Dictionary (like the ViewBag). – Linkgoron Mar 11 '11 at 02:13
  • I agree. Other languages have much better answers to this sort of problem (Perl, OCaml, F#). For C#, my approach is the style I'd use. Obviously I'm biased ;) – Ritch Melton Mar 11 '11 at 03:20
3

@pnvn has a great idea with the Unpack pattern, but it could be improved to iterate over the enumeration in a single pass, provide defaults past the end of the enumerable and work on any type or size of enumerable in a predictable way.

Here is an example using C# 7 out variables feature.

"A,B,C".Split(',')
    .Unpack(out string varA)
    .Unpack(out string varB);

This requires two extension methods, one for the IEnumerable to start, the second on the IEnumerator for each subsequent call.

public static IEnumerator<T> Unpack<T>(this IEnumerable<T> source, out T item)
{
    var enumerator = source.GetEnumerator();
    if (enumerator.MoveNext())
    {
        item = enumerator.Current;
        return enumerator;
    }
    item = default(T);
    return null;
}

public static IEnumerator<T> Unpack<T>(this IEnumerator<T> enumerator, out T item)
{
    if (enumerator != null && enumerator.MoveNext())
    {
        item = enumerator.Current;
        return enumerator;
    }            
    item = default(T);
    return null;
}
Josh Brown
  • 935
  • 1
  • 11
  • 21
3

And who can't resist some Linq insanity!

string names = "Joe,Bob,Lucy";
var myVars = names.Split(',').Select((x, index) => Tuple.Create(index,x)).ToDictionary(x => "var" + x.Item1, y => y.Item2);
Debug.WriteLine(myVars["var0"]);
gt124
  • 1,238
  • 13
  • 23
3

Or if you just want to avoid the extra variable name for readability sake, you could do something like that (C#7+):

public static void Deconstruct<T>(this IList<T> self, out T v1, out T v2) {
    v1 = self.Count > 0 ? self[0] : default;
    v2 = self.Count > 1 ? self[1] : default;
}

On another static class where you write your extension methods. And then use it like:

var (a, b) = "1,2".Split(','); // a = "1"; b = "2";

Obviously, this can be extended for more than two variables, but unfortunately, you have to write another method as far as I know.

For example:

public static class Ex {
    public static void Deconstruct<T>(this IList<T> self, out T v1, out T v2) {
        v1 = self.Count > 0 ? self[0] : default;
        v2 = self.Count > 1 ? self[1] : default;
    }

    public static void Deconstruct<T>(this IList<T> self, out T v1, out T v2, out T v3) {
        v1 = self.Count > 0 ? self[0] : default;
        v2 = self.Count > 1 ? self[1] : default;
        v3 = self.Count > 2 ? self[2] : default;
    }

    public static void Deconstruct<T>(this IList<T> self, out T v1, out T v2, out T v3, out T v4) {
        v1 = self.Count > 0 ? self[0] : default;
        v2 = self.Count > 1 ? self[1] : default;
        v3 = self.Count > 2 ? self[2] : default;
        v4 = self.Count > 3 ? self[3] : default;
    }
}

And use it like:

var (a,_,_,d) = "1a,2b,3c,4d".Split(','); // a = "1a"; d = "4d";

As a side effect, now you can deconstruct any array.

var (first,second) = new [] { 1,2 }; // first = 1; second = 2;
2

Not a one line solution. But, how about an extension method with an additional out parameter?

public static IEnumerable<T> Unpack<T>(this IEnumerable<T> source, out T target)
{
    target = source.First();
    return source.Skip(1);
}

Then, you could use the method like this.

string values = "aaa,bbb,ccc";
string x, y, z;
values.Split(',')
    .Unpack(out x)
    .Unpack(out y)
    .Unpack(out z);

Note that the Unpack method enumerates the sequence twice. So, I'd use it only if the data in the IEnumerable object is repeatable.

I didn't care to check the performance of the method because I thought that normally we would not unpack a large array.

Of course, you could use ref modifier instead of out, and the usage would be different.

pnvn
  • 488
  • 1
  • 5
  • 11
0

I couldn't resist adding to the ridiculousness :) Please don't use this "solution", at least not as-is.

static class StringExtensions
{
   private static readonly Func<object, string, Action<object, object>> GetSetter =
       (o1, n) => (o2, v) => o1.GetType().GetProperty(n).SetValue(o2, v, null);

   public static T AssignFromCSV<T>(this string csv, T obj, string[] propertyNames)
   {
       var a = csv.Split(new[] {','});
       for (var i = 0 ; i < propertyNames.Length; i++)
       {
           GetSetter(obj, propertyNames[i])(obj, a[i]);
       }
       return obj ;
   }
}

class TargetClass
{
   public string A { get; set; }

   public string B { get; set; }

   public string C { get; set; }
}

Usage:

var target = "1,2,3".AssignFromCSV(new TargetClass(), new[] {"A", "B", "C"}) ;
0

There is no built in way in C# to do a multiple assignment like you can in Perl; however, there are multiple solutions to get the data into each variable via a normal path.

Tejs
  • 40,736
  • 10
  • 68
  • 86
  • Well, what is the most elegant way to get the data into each variable? – Helge Klein Mar 10 '11 at 21:46
  • In my opinion, Rich Melton (above) posted a good solution - it abstracts the 'crap' (the splitting) behind a complex object and then names the resulting output variables into properties. – Tejs Mar 10 '11 at 21:48