I'm very happy with the performance, but I always try to optimize my code as much as possible. Since I plan on using the code in production, I feel more save posting it here first. I probably missed something obvious that could break everything.
Let's start with the KVF class. Its job is loading / saving values to the KVC class.
public class KVF :IDisposable
{
internal string Filename;
internal KVC Cache;
internal bool Debugoutput;
/// <summary>
/// Initializes KVF values.
/// Generates KVF file in memory for handling.
/// </summary>
/// <param name="filename">The file to load key/value pairs from.</param>
/// <param name="debugoutput">Specify whether the program is being debugged. <i>default: false</i></param>
public KVF(string filename,bool debugoutput = false, string kvsplit = "=")
{
Debugoutput = debugoutput;
if (debugoutput)
Console.WriteLine("[KVF] Concstructor Invoked. New File Handle: {0}", filename);
Filename = filename;
Cache = new KVC(this);
Cache.Generate(kvsplit);
}
/// <summary>Writes a value to the specified key (caller).</summary>
/// <param name="value">The value to write.</param>
/// <param name="caller">The calling member to write as key.</param>
public void MagicSave(object value, [CallerMemberName] string caller = null)
{
if (caller == null)
throw new Exception("Caller was null!");
if (Cache.Contains(caller))
Cache.KeyValue[caller] = value;
else
{
Cache.LookupTable.Add(caller);
Cache.KeyValue.Add(caller, value);
}
if (Debugoutput)
Console.WriteLine("[KVF] MagicSave Invoked: {0}={1}", caller, value);
}
/// <summary>Writes a value using the specified value to the specified key.</summary>
/// <param name="key">The key containg the value.</param>
/// <param name="value">The value to write.</param>
public void Save(string key, object value)
{
if (key == null)
throw new Exception("Key was null!");
if (Cache.Contains(key))
Cache.KeyValue[key] = value;
else
{
Cache.LookupTable.Add(key);
Cache.KeyValue.Add(key, value);
}
if (Debugoutput)
Console.WriteLine("[KVF] Save Invoked: {0}={1}", key, value);
}
public T MagicLoad<T>([CallerMemberName] string key = null)
{
if (key == null)
throw new Exception("Caller was null!");
if (Debugoutput)
Console.WriteLine("[KVF] MagicLoad Invoked: {0}", key);
if (Cache.Contains(key))
return (T)Convert.ChangeType(Cache.KeyValue[key], typeof(T));
Cache.KeyValue.Add(key, default(T));
Cache.LookupTable.Add(key);
return default(T);
}
public T MagicLoad<T>(T defaultval,[CallerMemberName] string key = null)
{
if (key == null)
throw new Exception("Caller was null!");
if (Debugoutput)
Console.WriteLine("[KVF] MagicLoad Invoked: {0}", key);
if (Cache.Contains(key))
return (T)Convert.ChangeType(Cache.KeyValue[key], typeof(T));
Cache.KeyValue.Add(key, defaultval);
Cache.LookupTable.Add(key);
return defaultval;
}
/// <summary>Reads a value from the speified key. Returns as specified data type (T).</summary>
/// <typeparam name="T">The data type to return as.</typeparam>
/// <param name="key">The key to read from.</param>
public T Load<T>(string key)
{
if (Debugoutput)
Console.WriteLine("[KVF] Load Invoked: {0}", key);
if (Cache.Contains(key))
return (T)Convert.ChangeType(Cache.KeyValue[key], typeof(T));
Cache.LookupTable.Add(key);
Cache.KeyValue.Add(key, default(T));
return default(T);
}
public T Load<T>(string key,T defaultval)
{
if (Debugoutput)
Console.WriteLine("[KVF] Load Invoked: {0}", key);
if (Cache.Contains(key))
return (T)Convert.ChangeType(Cache.KeyValue[key], typeof(T));
Cache.KeyValue.Add(key, default(T));
Cache.LookupTable.Add(key);
return defaultval;
}
/// <summary>Rebuilds the KVF cache.</summary>
public void RefreshCache()
{
if (Debugoutput)
Console.WriteLine("[KVF] Rebuilding Cache: " + Filename);
Cache.KeyValue.Clear();
Cache.LookupTable.Clear();
Cache.Generate();
}
/// <summary>Commits the cache to the hard drive.</summary>
public void Commit()
{
Cache.Commit();
}
/// <summary>Disposes of KVF and KVC resources.</summary>
public void Dispose()
{
if (Debugoutput)
Console.WriteLine("[KVF] Disposing: " + Filename);
Cache.Commit();
Cache.File = null;
Cache = null;
}
}
The KVC class (Key Value Cache) keeps everything in RAM, and writes it out on disposal.
internal class KVC
{
internal KVF File;
internal string Splitter;
internal readonly Dictionary<string, object> KeyValue;
internal readonly HashSet<string> LookupTable;
/// <summary>
/// Initializes a new KVF cache to handle the file in memory.
/// Creates file if non-existent.
/// </summary>
/// <param name="file">The KVF file to generate the cache for.</param>
public KVC(KVF file)
{
if (!System.IO.File.Exists(file.Filename))
System.IO.File.Create(file.Filename).Close();
File = file;
KeyValue = new Dictionary<string, object>(StringComparer.Ordinal);
LookupTable = new HashSet<string>(StringComparer.Ordinal);
if (File.Debugoutput)
Console.WriteLine("[KVC] Concstructor Invoked. New Cache Handle: {0}", File.Filename);
}
/// <summary>Generates the file for handling in memory.</summary>
public void Generate(string splitter = "=")
{
Splitter = splitter;
using (var stream = System.IO.File.OpenText(File.Filename))
{
while (!stream.EndOfStream)
{
var line = stream.ReadLine();
// ReSharper disable once PossibleNullReferenceException
var keyvalue = line.Split(new [] { splitter },StringSplitOptions.RemoveEmptyEntries);
if (keyvalue.Length < 2)
continue;
KeyValue.Add(keyvalue[0], keyvalue[1]);
LookupTable.Add(keyvalue[0]);
}
}
}
public bool Contains(string value)
{
return Enumerable.Contains(LookupTable, value);
}
/// <summary>Flushes the cache to the hard drive.</summary>
public void Commit()
{
using (var stream = System.IO.File.Open(File.Filename, FileMode.Truncate, FileAccess.Write))
{
using (var writer = new StreamWriter(stream))
{
writer.WriteLine("--- KVF File Format 1.2 by Yuki ---");
foreach (var kvp in KeyValue)
{
writer.WriteLine("{0}"+Splitter+"{1}", kvp.Key, kvp.Value);
}
}
}
}
}
I don't mind exceptions on parsing, e.g. no valid splitter. My main concern is speed. I can parse 12.000 of those (http://pastebin.com/amQW5T1Z) in 600ms. That's 2.3x faster than Interop INI (Read/WriteProfileString).