Note: I am using Unity3D game engine, which uses .Net 2.0 (.Net 2.0.50727.1433 to be precise) to compile code, so I don't have access to newest features of C# 6, or even C# 5. Please keep this in mind while writing your reviews.
What is this?
This (DataTable.cs) is a dictionary that takes two keys, and returns a value; along with a special implementation (SymmetricDataTable.cs) of this dictionary that is key-order agnostic.
What is this for?
This is for a diplomacy manager in a strategy game I am developing. I give it two nations, it gives me back the diplomatic relation between those nations.
What I want to be reviewed?
Everything.
Teminology I used in my code:
DataTable: Class name of my implementation of a 2D dictionary.
Symmetric DataTable: Key-order agnostic version of DataTable class.
Row: Alias for first key in DataTable.
Col: Short for Column, alias for second key in DataTable.
Cell: A value that corresponds to a specific Row and Col combination.
So here is my implementation that consists of two classes and one struct:
DataTable.cs
using System.Collections;
using System.Collections.Generic;
namespace UnityCSCommon.Expansions
{
/// <summary>
/// A fast collection that consists of rows and columns, and cells that corresponds to intersection of these.
/// </summary>
/// <typeparam name="TRow">The type of rows.</typeparam>
/// <typeparam name="TCol">The type of columns.</typeparam>
/// <typeparam name="TCell">The type of cells (values).</typeparam>
///
/// <remarks>
/// In this type, we treat all references as values and use integers as keys to access them.
/// The way this is possible is:
/// 1- We get hash codes of both row and column.
/// 2- We use a pairing function to generate a unique number out of two hash codes.
/// 3- We use the unique number as the key for the entry.
///
/// So, if user didn't make something stupid like overriding the GetHashCode() method with a constant,
/// we will get the same unique number for the same row and column every time. Thus, the same entry.
///
/// I was using Cantor Pairing at first, but then I discovered Szudzik's Elegant Pairing,
/// which has better coverage. So I switched to it, but I still keep Cantor Pairing in
/// it's own method in case I need it.
/// </remarks>
public class DataTable<TRow, TCol, TCell> : IEnumerable<DataTableEntry<TRow, TCol, TCell>>
{
private static readonly TCell DefaultCellValue = default(TCell);
private readonly Dictionary<long, DataTableEntry<TRow, TCol, TCell>> _entries = new Dictionary<long, DataTableEntry<TRow, TCol, TCell>>();
/// <summary>
/// Sets the intersection of <paramref name="row"/> and <paramref name="col"/> to <paramref name="newCell"/>.
/// </summary>
public void Set (TRow row, TCol col, TCell newCell)
{
long key = CalculateKey (row, col);
var newEntry = CreateEntry (row, col, newCell);
if (_entries.ContainsKey (key))
{
_entries[key] = newEntry;
}
else
{
_entries.Add (key, newEntry);
}
}
/// <summary>
/// Returns the intersection of <paramref name="row"/> and <paramref name="col"/>. <para/>
/// Throws a <see cref="KeyNotFoundException"/> if no such cell exists (i.e. it has never set).
/// </summary>
public TCell Get (TRow row, TCol col)
{
long key = CalculateKey (row, col);
DataTableEntry<TRow, TCol, TCell> value;
if (_entries.TryGetValue (key, out value))
{
return value.Cell;
}
else
{
throw new KeyNotFoundException ("This cell is never set.");
}
}
/// <summary>
/// Tries to get the intersection of <paramref name="row"/> and <paramref name="col"/> and assigns it to <paramref name="cell"/>. <para/>
/// Returns true if a cell is found, false otherwise (i.e. it has never set). <para/>
/// Note: If return is false, the value of <paramref name="cell"/> will be the default value of <typeparamref name="TCell"/>.
/// </summary>
public bool TryGet (TRow row, TCol col, out TCell cell)
{
long key = CalculateKey(row, col);
DataTableEntry<TRow, TCol, TCell> value;
if (_entries.TryGetValue (key, out value))
{
cell = value.Cell;
return true;
}
else
{
cell = DefaultCellValue;
return false;
}
}
public TCell this [TRow row, TCol col]
{
get { return Get (row, col); }
set { Set (row, col, value); } // Note for CodeReview SE: *value* is a keyword here, but snippet highlighter doesn't think so.
}
protected virtual long CalculateKey (TRow row, TCol col)
{
int rowHash = row.GetHashCode();
int colHash = col.GetHashCode();
return Pair (rowHash, colHash);
}
protected static long Pair (int x, int y)
{
return SzudzikPairing (x, y);
}
private static long SzudzikPairing (int x, int y)
{
// Szudzik's Elegant Pairing Function
return x >= y ? x*x + x + y : y*y + x;
}
// Cantor Pairing is no longer in use since Szudzik's Pairing gives better coverage.
// But I am still keeping it in case we need it later.
private static long CantorPairing (int x, int y)
{
// Cantor Pairing Function
return (x + y)*(x + y + 1)/2 + y;
}
private static DataTableEntry<TRow, TCol, TCell> CreateEntry (TRow row, TCol col, TCell cell)
{
return new DataTableEntry<TRow, TCol, TCell> (row, col, cell);
}
#region Implementation of IEnumerable
public IEnumerator<DataTableEntry<TRow, TCol, TCell>> GetEnumerator()
{
return _entries.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
}
DataTableEntry.cs
namespace UnityCSCommon.Expansions
{
/// <summary>
/// The value type of the internal dictionary of a <see cref="DataTable{TRow,TCol,TCell}"/> class.
/// </summary>
public struct DataTableEntry<TRow, TCol, TCell>
{
private readonly TRow _row;
private readonly TCol _col;
private readonly TCell _cell;
public TRow Row
{
get { return _row; }
}
public TCol Col
{
get { return _col; }
}
public TCell Cell
{
get { return _cell; }
}
public DataTableEntry (TRow row, TCol col, TCell cell)
{
_row = row;
_col = col;
_cell = cell;
}
}
}
SymmetricDataTable.cs
namespace UnityCSCommon.Expansions
{
/// <summary>
/// A special <see cref="DataTable{TRow,TCol,TCell}"/> type where you get the same cell when you swap the row and column. <para/>
/// For example: When you assign value A to [1-2], you will get value A for both [1-2] and [2-1]. ([1-2] means row 1 and column 2) <para/>
/// To be able to swap row and column, they have to be the same type. So this class takes a single type for both rows and columns.
/// </summary>
///
/// <remarks>
/// Implementing this class is really easy. While getting the key before pairing function, we sort the hash values, so it doesn't matter which hash is which object's.
/// For example, say we have object A and object B. Let hash code of A be 1 and B be 2.
/// - CalculateKey (A, B) --> Pair (Max (A,B), NotMax (A,B)) = Pair (2,1).
/// - CalculateKey (B, A) --> Pair (Max (B,A), NotMax (B,A)) = Pair (2,1).
/// As you see, the pair remains same no matter the order of objects.
/// </remarks>
public class SymmetricDataTable<TRowAndCol, TCell> : DataTable<TRowAndCol, TRowAndCol, TCell>
{
#region Overrides of DataTable<TRow,TCol,TCell>
protected override long CalculateKey (TRowAndCol row, TRowAndCol col)
{
int rowHash = row.GetHashCode();
int colHash = col.GetHashCode();
// This is a sorting operation. If colHash is bigger, we swap them. So first parameter is always the bigger one.
return rowHash >= colHash ? Pair (rowHash, colHash) : Pair (colHash, rowHash);
}
#endregion
}
}