7

I've got a group of data that looks like this:

001 001 One
001 002 Two
001 003 Three

002 001 One
002 002 Two
002 003 Three

...

Now, certainly, I could create an array of string[x][y] = z, but this array has to be resizable, and i'd prefer to use the string representations of the indexers than convert to numeric. The reason is that i will need to look up the data by string, and i don't see the point in needless string->number conversions.

My first thought was this:

Dictionary<string, Dictionary<string, string>> data;

data = new Dictionary<string, Dictionary<string, string>>();

Dictionary<string, string> subdata = Dictionary<string, string>();

subdata.Add(key, string);
data.add(key2, subdata);

and this works, but is somewhat cumbersome. It also feels wrong and kludgy and not particularly efficient.

So what's the best way to store this sort of data in a collection?

I also thought of creating my own collection class, but I'd rather not if I don't have to. I'd rather just use the existing tools.

r5d
  • 579
  • 5
  • 24
Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • You said they might not be sequential, but are they always ordered? Your choice of list versus dictionary is affected by this, although you can always have an enumerator that sorts before being enumerated. – Robert Paulson Jul 23 '09 at 02:54
  • List can only be indexed sequentially. If you delete an item from the list, then item + 1 becomes item - 1 in the index, so this doesn't work. They have to be indexed by their values. – Erik Funkenbusch Jul 23 '09 at 02:57
  • I see a lot of your comments are about avoiding writing a wrapper 'for maintenance' reasons. You should reconsider that stance, because you'll likely find that writing a wrapper class makes it easier to code, test, and maintain in the long term and not the other way around. Encapsulation is a great facility of OO programming. Otherwise you end up pushing all the logic and add unnecessarily coupling to the consumer code, as well as tightly couple storage and implementation. Write your test cases before you write the wrapper, and it'll be quite evident what interface you need to maintain. – Robert Paulson Jul 23 '09 at 05:08
  • My stance on wrapper code goes well beyond just maintenance reasons. I am of the DRY principle mindset, and I find wrapper code to quite often violate this principle (even though you're not repeating the code inside, you're repeating the boilerplate). I prefer solutions in which I don't have to wrap. Ryan's solution, for instance, doesn't require any wrapper code at all and achieves teh same result. I find this to be a superior solution. – Erik Funkenbusch Jul 23 '09 at 06:37
  • Further, a lot of work and effort has gone into the BCL collection classes, and their access methods are well researched and (hopefully) bug free. If I have to recreate a lot of that code, simple statistics will say I won't create it anywhere near as well unless i spend similar time designing them. It's real easy to turn something that's O(1) into O(n) or O(n^2) or O(log n) or worse if you're not paying attention. Also, when you encapsulate something with a lot of functionality, like Dictionary, you have to write a lot of wrappers if you need that functionality. – Erik Funkenbusch Jul 23 '09 at 06:46
  • Forcing the users of your library to pass around Dictionary> isn't going to curry any favor with them, especially when it binds their code to your implementation of a data structure. Good OO practices say you don't do that. The class you make should still use the generic collection classes internally, so you will not affect the big-O characteristics, nor are you likely to introduce bugs. IList is something like 9 methods. I did a wrapper the other day because I imposed different semantics on my list, and it took me all of 10 minutes including unit tests. – Robert Paulson Jul 25 '09 at 21:52

6 Answers6

6

This is pretty common request, and most people end up writing some variation of a Tuple class. If you're using ASP.Net, you can utilize the Triple class that's already available, otherwise, write something like:

public class Tuple<T, T2, T3>
{
    public Tuple(T first, T2 second, T3 third)

    {
        First = first;
        Second = second;
        Third = third;
    }

    public T First { get; set; }
    public T2 Second { get; set; }
    public T3 Third { get; set; }

}

There's a generic three-tuple class, so you can create a new List<Tuple<string, string, string>>() and create your tuples and add them. Expand on that basic class with some indexing functionality and you're up up and away.

Edit: A list with a dictionary doesn't seem like the correct approach, because each dictionary is only holding one value. There is no multi-entry relationship between the key and values - there is simply one multi-part key and one associated value. The data is equivalent to a database row (or tuple!).

Edit2: Here's an indexable list class you could use for convenience.

    public class MyTupleList : List<Tuple<string, string, string>>
    {
        public Tuple<string, string, string> this[string first, string second]
        {
            get
            {
                return (this.Find(x => x.First == first && x.Second == second));
            }
            set
            {
                this[first, second] = value;
            }
        }
    }
womp
  • 115,835
  • 26
  • 236
  • 269
  • I had thought about this, but I couldn't see a good way to lookup items without walking the entire list and comparing values. You could do it with LINQ, but i can't be sure that this will always be used with C# 3 or greater. This would mean I'd have to add all the lookup searching code which, as you probably know, is not easy to get correct. – Erik Funkenbusch Jul 23 '09 at 03:00
  • I'll give you a full example in a minute, you'll love it ;) – womp Jul 23 '09 at 03:09
  • Thanks for the nice example. But, as I said in my comment, I can't be sure it will always be used with C# 3, so LINQ, while a nice solution to the problem, is not an option. And while an interesting exercise, I really don't want to spend my time writing good indexing code, that's what the existing collection classes are supposed to help us avoid ;) – Erik Funkenbusch Jul 23 '09 at 03:30
  • Well it depends on how flexible you need the key lookups. This is really just a generic version of what Ryan and John are proposing, which for your purposes are probably the best answers here, the indexing example is just icing, it's not even necessary. And it's simple to do the same thing without LINQ ;) – womp Jul 23 '09 at 03:44
  • I think i'm going to give you the answer anyways, because it's a good solution. – Erik Funkenbusch Jul 23 '09 at 04:00
  • you need the code to compile on different versions of the compiler or to run on different versions of the runtime? C#3 compiled DLL/programs run on the v2 of the runtime. Linq is syntactic suggar and is just compiled into regular static method calls. (All extension methods are syntactic sugar compiled into static method calls) – Rune FS Jul 23 '09 at 05:45
  • Compile on different versions of the compiler. This is generic, by definition, so it needs to be able to create the types at compile time. – Erik Funkenbusch Jul 23 '09 at 06:38
4

I think this really depends on what you are modelling here. If you're planning to use an object-oriented approach, you shouldn't be thinking of these as arbitrary items inside a data structure.

I'm guessing from looking at this that the first two columns are serving as a "key" for the other items. Define a simple struct, and create a dictionary of like so:

struct Key {
   public int Val1 { get; set; }
   public int Val2 { get; set; }
}

....

Dictionary<Key, string> values;

Obviously Key and the items inside it should be mapped to something closer to what you are representing.

Ryan Brunner
  • 14,723
  • 1
  • 36
  • 52
  • I'm not sure that does what you intend it to. Or maybe it does because you're using a struct (a value type). Are all key.Val1, key.Val2's equal if they contain the same data? – Erik Funkenbusch Jul 23 '09 at 02:53
  • 1
    I like this approach. You'd probably need to implement IComparer or Equals. – Randolpho Jul 23 '09 at 02:55
  • That is, Does this return true? new Key {Val1 = 1, Val2 = 2} == new Key {Val1 = 1, Val2 = 2}; – Erik Funkenbusch Jul 23 '09 at 02:55
  • 1
    Yeah, sorry - you would need to implement IComparer and equals. I'm not 100% sure on the key evaluating, but I'm fairly sure it would work. Needs testing and all that jazz. – Ryan Brunner Jul 23 '09 at 02:57
  • 1
    This approach is very similar to the approaches specified by both myself and dtb in our edits, but it uses a structure rather than a string concatenation. The example above is incomplete; IComparer and/or Equals needs to be implemented/overridden, but other than that, it's a very solid approach. – Randolpho Jul 23 '09 at 03:06
  • GetHashcode would likely need to be overridden as well, given the fact that Dictionary will probably use a hashtable internally. – Randolpho Jul 23 '09 at 03:07
  • Yes, this looks like a better solution than your encapsulation approach in a new class. It solves the problem of getting access to the full complement of Dictionary functionality without writing a lot of wrapper code. – Erik Funkenbusch Jul 23 '09 at 03:26
3

Given a suitable Pair<A,B> class*, left as an exercise for the reader, you could use a Dictionary<Pair<string, string>, string>.

* A class with equality and hash code overrides, nothing terribly hard.

Community
  • 1
  • 1
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • for this particular question, one could use Point (https://learn.microsoft.com/en-us/dotnet/api/system.drawing.point) instead of creating it so it becomes Dictionary()... Point is a struct of two doubles, retrieve like: dictionary[new Point(x,y)] – A. Harkous Apr 29 '20 at 18:32
1

Would a List<List<T>> work for you? Still kludgy, but better than dictionaries IMHO.


EDIT: What about a Dictionary<string,string> and mapping the two keys to a single string?

var data = new Dictionary<string,string>(StringComparer.Ordinal);

data[GetKey("002", "001")] = "One";

with

string GetKey(string a, string b) {
    return a + "\0" + b;
}
dtb
  • 213,145
  • 36
  • 401
  • 431
  • No. As I said, I would prefer to index them by string. And they may not always be sequential (might have a missing number in the middle). – Erik Funkenbusch Jul 23 '09 at 02:52
  • 2
    Oh, ok... I think your mention of multi-dimensional arrays threw us off (since those wouldn't have been indexed by Strings either). – jsight Jul 23 '09 at 02:56
1

List<List<string>> is really your best bet in this case. But I agree, it's kludgy. Personally, I would create a custom class that implements a two-dimensional indexer and maybe use a List<List<T>> internally.

For example:

public class DynamicTwoDimensonalArray<T>
{
  private List<List<T>> Items = new List<List<T>>();

  public T this[int i1, int i2]
  {
    get
    {
      return Items[i1][i2];
    }
    set
    {
      Items[i1][i2] = value;
    }
  }  
}

This is a basic idea to get you going; clearly the setter needs to deal with bounds issues. But it's a start.

Edit:

No. As I said, I would prefer to index them by string. And they may not always be sequential (might have a missing number in the middle). - Mystere Man

Hmm... this is interesting. If that's the case, your best bet would be to create some sort of concatenation of the combination of the two indexers and use that as the key in a single-level dictionary. I would still use a custom class to make using the indexing easier. For example:

public class TwoDimensionalDictionary
{
  private Dictionary<string, string> Items = new Dictionary<string, string>();

  public string this[string i1, string i2]
  {
    get
    {
      // insert null checks here
      return Items[BuildKey(i1, i2)];
    }
    set
    {
      Items[BuildKey(i1, i2)] = value;
    }
  }
  public string BuildKey(string i1, string i2)
  {
    return "I1: " + i1 + " I2: " + i2;
  }  
}
Randolpho
  • 55,384
  • 17
  • 145
  • 179
  • This is an interesting solution, but i see some problems with edge cases. They could be solved though. For instance, if i1,i2 doesn't exist, it will throw an exception. no way to get access to the "Try" functionality or to do a if (Items.Keys.Exists(..)) kind of deal. so it would involve writing a lot of wrapper code, which i'm trying to avoid for maintenance reasons. – Erik Funkenbusch Jul 23 '09 at 03:24
  • Very true. As I said, it's a start, not a solution. That said, you'll have the same problems you mentioned even if you go the Dictionary> route. Using a custom class to handle all of this will make your code much more maintainable. – Randolpho Jul 23 '09 at 03:49
0

If you are ever going to need to find z by given (x,y) (and not, for example, find all y by given x), then use this:

Dictionary<KeyValuePair<string, string>, string>

Otherwise, your dictionary is fine as is.

Pavel Minaev
  • 99,783
  • 25
  • 219
  • 289