NOTE
I've come up with two .NET solutions to this scenario. Because both solutions require the same JavaScript code, I've only included the JavaScript code for the 2 solutions in this answer.
Now on to the answer...
EDIT Note: I apologize for not completely understanding your question at first. I never heard the term "Sparse Array" used before, so I had to look it up and found the text book definition from Wikipedia, which isn't quite what you were describing and, as far as usage I've seen elsewhere since then, doesn't seem to be what you were describing. It does make sense to use it in this way however.
Thinking about this scenario more has driven me to come up with a solution. As you mentioned, C# (and .NET in general) has no concept of undefined
, other than null
.
From what I understand, you're looking to be able to represent three different things in your array:
- A valid value for the array element
- A null value for the array element
- The concept of an "undefined" array element
As you pointed out, C# has no concept of "undefined", other than null
. To .NET, undefined
would be the same as null
and it probably should be kept that way. But since you obviously need a way to represent this anyway (I'm assuming for some strange business rule), I tried to come up with an implementation that will work. I'd strongly recommend not doing this though!
The JavaScript Side
The first challenge is the act of serializing the array in JavaScript. When converting an array to JSON format, undefined
elements in an array are automatically converted to null
. If you call JSON.stringify(...)
, this will happen:
var array = [73,42,undefined,null,23];
var json = JSON.stringify(array);
if(json === "[73,42,null,null,23]")
alert('I am always true!');
The example above shows that undefined elements in the array are serialized as "null
". If you really need to store undefined, you'd have to write a conversion method that will manually serialize arrays to have undefined elements. The ideal solution would result in this JSON:
"[73,42,undefined,null,23]"
This will not work however. Neither the .NET JavaScriptSerializer
, or JSON.parse(...)
can parse this string (although JavaScript's eval(...)
method will work). What we need is a way to represent "undefined
" that will serialize using standard JSON methods, and can be understood by .NET's JavaScriptSerializer
. The trick to doing this is coming up with an actual object to represent undefined. To represent this, I used this JSON string:
"{undefined:null}"
An object whose only property is called "undefined", and whose value is null
. This most likely will not exist in any object in JavaScript code so I'm using this as I consider it a "unique" enough flag to represent our non-existent "undefined" array element.
To serialize JSON like this, you need to provide a replacer that will do the job, so here is how you'd do that:
var arr = [73,42,undefined,null,23];
var json = JSON.stringify(arr, function(key, value) {
jsonArray = value;
if(value instanceof Array) {
var jsonArray = "[";
for(var i = 0; i < value.length; i++) {
var val = value[i];
if(typeof val === "undefined") {
jsonArray += "{undefined:null}";
} else {
jsonArray += JSON.stringify(value[i]);
}
if(i < value.length - 1) {
jsonArray += ",";
}
}
jsonArray += "]";
if(key != null && key != "") {
return key + ":" + jsonArray;
} else {
return jsonArray;
}
}
if(key != null && key != "") {
return key + ":" + JSON.stringify(jsonArray);
} else {
return JSON.stringify(jsonArray);
}
});
This will get you a JSON string that looks like this:
"[73,42,{undefined:null},null,23]"
The downside to this is that once it is deserialized, the undefined
array element is no longer undefined
, but another object. This means that your parser will have to handle this specially. Make sure that you don't try to use this in any JSON parser that isn't aware of the completely made up "undefined" object. The next step is handle it in C#.
The C# Side
The challenge here is how to represent Undefined values. The solution below opts to treat undefined indexes as "non-existent" in an array. My other solution creates a wrapper similar to the Nullable<T>
wrapper for structs.
Create a SparseArray<T>
Dictionary Wrapper
I created a SparseArray<T>
class that can be used just like a normal array in .NET. The difference between this class & a normal array is that when you access an array element which is undefined, it throws a custom IndexNotFoundException
. You can avoid throwing this exception by first checking if the element is defined by calling SparseArray<T>.ContainsIndex(index)
to check if it has that index.
Internally it uses a Dictionary, as you mentioned in your question. When you initialize it, first the constructor takes the JSON string and runs it through a JavaScriptSerializer.
It then takes the deserialized array and begins to add each array element to its _Array
dictionary. As it adds elemnts, it looks for the {undefined:null}
object we defined in JavaScript when we "strungified" it (not sure if that's the correct past tense there...). If it sees this object, the index is skipped over. The length of the array will increment as undefined values are found, however their index is skipped over.
Since the class' code is rather long, I'll put the usage example first:
string json = "[4,5,null,62,{undefined:null},1,68,null, 3]";
SparseArray<int?> arr = new SparseArray<int?>(json);
In a for { }
loop, you'll need to check if the array contains each index before accessing it. In a foreach { }
loop, the enumerator holds only defined values, so you won't need to worry about the occurrence of undefined
values.
Here is the code for my SparseArray<T>
class:
[DebuggerDisplay("Count = {Count}")]
public class SparseArray<T>: IList<T>
{
Dictionary<int, T> _Array = new Dictionary<int, T>();
int _Length = 0;
public SparseArray(string jsonArray)
{
var jss = new JavaScriptSerializer();
var objs = jss.Deserialize<object[]>(jsonArray);
for (int i = 0; i < objs.Length; i++)
{
if (objs[i] is Dictionary<string, object>)
{
// If the undefined object {undefined:null} is found, don't add the element
var undefined = (Dictionary<string, object>)objs[i];
if (undefined.ContainsKey("undefined") && undefined["undefined"] == null)
{
_Length++;
continue;
}
}
T val;
// The object being must be serializable by the JavaScriptSerializer
// Or at the very least, be convertible from one type to another
// by implementing IConvertible.
try
{
val = (T)objs[i];
}
catch (InvalidCastException)
{
val = (T)Convert.ChangeType(objs[i], typeof(T));
}
_Array.Add(_Length, val);
_Length++;
}
}
public SparseArray(int length)
{
// Initializes the array so it behaves the same way as a standard array when initialized.
for (int i = 0; i < length; i++)
{
_Array.Add(i, default(T));
}
_Length = length;
}
public bool ContainsIndex(int index)
{
return _Array.ContainsKey(index);
}
#region IList<T> Members
public int IndexOf(T item)
{
foreach (KeyValuePair<int, T> pair in _Array)
{
if (pair.Value.Equals(item))
return pair.Key;
}
return -1;
}
public T this[int index]
{
get {
if (_Array.ContainsKey(index))
return _Array[index];
else
throw new IndexNotFoundException(index);
}
set { _Array[index] = value; }
}
public void Insert(int index, T item)
{
throw new NotImplementedException();
}
public void RemoveAt(int index)
{
throw new NotImplementedException();
}
#endregion
#region ICollection<T> Members
public void Add(T item)
{
_Array.Add(_Length, item);
_Length++;
}
public void Clear()
{
_Array.Clear();
_Length = 0;
}
public bool Contains(T item)
{
return _Array.ContainsValue(item);
}
public int Count
{
get { return _Length; }
}
public bool IsReadOnly
{
get { return false; }
}
public bool Remove(T item)
{
throw new NotImplementedException();
}
public void CopyTo(T[] array, int arrayIndex)
{
throw new NotImplementedException();
}
#endregion
#region IEnumerable<T> Members
public IEnumerator<T> GetEnumerator()
{
return _Array.Values.GetEnumerator();
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _Array.Values.GetEnumerator();
}
#endregion
}
And the exception needed to use this class:
public class IndexNotFoundException:Exception
{
public IndexNotFoundException() { }
public IndexNotFoundException(int index)
: base(string.Format("Array is undefined at position [{0}]", index))
{
}
}