I have a cache that I implement using a ConcurrentDictionary, The data that I need to keep depends on 5 parameters. So the Method to get it from the cache is: (I show only 3 parameters here for simplicity, and I changed the data type to represent CarData for clearity)
public CarData GetCarData(string carModel, string engineType, int year);
I wonder what type of key will be better to use in my ConcurrentDictionary, I can do it like this:
var carCache = new ConcurrentDictionary<string, CarData>();
// check for car key
bool exists = carCache.ContainsKey(string.Format("{0}_{1}_{2}", carModel, engineType, year);
Or like this:
var carCache = new ConcurrentDictionary<Tuple<string, string, int>, CarData>();
// check for car key
bool exists = carCache.ContainsKey(new Tuple(carModel, engineType, year));
I don't use these parameters together any other place, so there is no justification to create a class just to keep them together.
I want to know which approach is a better in terms of performance and maintainability.
I ran Tomer's test cases, adding ValueTuples as a test case (new c# value type). Was impressed at how well they performed.
Code for the test is below:
If performance is really important, then the answer is that you shouldn't use either option, because both unnecessarily allocate an object on every access.
Instead, you should use a
struct
, either a custom one, orValueTuple
from the System.ValueTuple package:C# 7.0 also contais syntax sugar to make this code easier to write (but you don't need to wait for C# 7.0 to start using
ValueTuple
without the sugar):As always, you have the tools to figure it out. Code both possible solutions and make them race. The one that wins is the winner, you don't need anyone here to answer this particular question.
About maintenance, the solution that autodocuments itself better and has better scalability should be the winner. In this case, the code is so trivial that autodocumentation isn't that much of an issue. From a scalability point of view, IMHO, the best solution is to use
Tuple<T1, T2, ...>
:Collisions are not possible, something that is not true if you choose the string concatenation solution:
Yeah, far fetched, but, in theory, entirely possible and your question is precisely about unknown events in the future, so...
And last, but not least, the compiler helps you maintain the code. If, for example, tomorrow you have to add
param4
to your key,Tuple<T1, T2, T3, T4>
will strongly type your key. Your string concatenation algorithm on the other hand can live on blissfully happy generating keys withoutparam4
and you wont know whats happening until your client calls you up because their software is not working as expected.IMHO, I prefer to use in such cases some intermediate structure (in your case it will be
Tuple
). Such approach creates additional layer between parameters and end-target dictionary. Of course, it will be depend on purposes. Such way for example allows you to create not trivial transition of parameters (for example container may "distort" data).Implement a custom key class and make sure it is suitable for such use cases, i.e. implement
IEquatable
and make the class immutable:This is a
GetHashCode()
implementation how Resharper generates it. It is a good general-purpose implementation. Adapt as required.Alternatively, use something like Equ (I'm the creator of that library) that automatically generates
Equals
andGetHashCode
implementations. This will make sure that these methods always include all members of theCacheKey
class, so the code becomes much easier to maintain. Such an implementation would then simply look like this:Note: You should obviously use meaningful property names, otherwise introducing a custom class does not provide much benefit over using a
Tuple
.I wanted to compare the
Tuple
versusClass
versus "id_id_id" approaches described in the other comments. I used this simple code:Results:
I ran each method 3 times in Release without debugging, each run commenting out the calls to the other methods. I took the average of the 3 runs, but there wasn't much variance anyway.
TestTuple:
TestClass:
TestFlat:
I was surprised to see that the class approach was faster than both the tuple approach and the string approach. In my opinion it's more readable and more future-safe, in the sense that more functionality can be added to the
Key
class (assuming it's not just a key, it represents something).