diff --git a/src/NHibernate.Test/Async/UtilityTest/LinkedHashMapFixture.cs b/src/NHibernate.Test/Async/UtilityTest/LinkHashMapFixture.cs similarity index 73% rename from src/NHibernate.Test/Async/UtilityTest/LinkedHashMapFixture.cs rename to src/NHibernate.Test/Async/UtilityTest/LinkHashMapFixture.cs index ff1e2011664..99fcfedf320 100644 --- a/src/NHibernate.Test/Async/UtilityTest/LinkedHashMapFixture.cs +++ b/src/NHibernate.Test/Async/UtilityTest/LinkHashMapFixture.cs @@ -9,6 +9,7 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; @@ -19,17 +20,21 @@ namespace NHibernate.Test.UtilityTest { using System.Threading.Tasks; [TestFixture] - public class LinkedHashMapFixtureAsync + public class LinkHashMapFixtureAsync { - private static readonly Player[] players = { - new Player("12341", "Boeta Dippenaar"), new Player("23432", "Gary Kirsten"), - new Player("23411", "Graeme Smith"), new Player("55221", "Jonty Rhodes"), - new Player("61234", "Monde Zondeki"), new Player("23415", "Paul Adams") - }; + private static readonly Player[] players = + { + new Player("12341", "Boeta Dippenaar"), + new Player("23432", "Gary Kirsten"), + new Player("23411", "Graeme Smith"), + new Player("55221", "Jonty Rhodes"), + new Player("61234", "Monde Zondeki"), + new Player("23415", "Paul Adams") + }; private static void Fill(IDictionary lhm) { - foreach (Player player in players) + foreach (var player in players) lhm.Add(player.Id, player); } @@ -37,11 +42,11 @@ private static void Fill(IDictionary lhm) public async Task ShowDiffAsync() { IDictionary dict = new Dictionary(); - IDictionary lhm = new LinkedHashMap(); + IDictionary lhm = new LinkHashMap(); Fill(dict); Fill(lhm); // Override the first element - Player o = new Player("12341", "Ovirride"); + var o = new Player("12341", "Override"); dict[o.Id] = o; lhm[o.Id] = o; await (Console.Out.WriteLineAsync("Dictionary order:")); @@ -49,7 +54,7 @@ public async Task ShowDiffAsync() { Console.Out.WriteLine("Key->{0}", pair.Key); } - await (Console.Out.WriteLineAsync("LinkedHashMap order:")); + await (Console.Out.WriteLineAsync("LinkHashMap order:")); foreach (KeyValuePair pair in lhm) { Console.Out.WriteLine("Key->{0}", pair.Key); @@ -65,18 +70,18 @@ public async Task PerformanceAsync() int numOfEntries = Int16.MaxValue; - long[] dictPopulateTicks = new long[numOfRuns]; - long[] dictItemTicks = new long[numOfRuns]; + var dictPopulateTicks = new long[numOfRuns]; + var dictItemTicks = new long[numOfRuns]; - long[] linkPopulateTicks = new long[numOfRuns]; - long[] linkItemTicks = new long[numOfRuns]; + var linkPopulateTicks = new long[numOfRuns]; + var linkItemTicks = new long[numOfRuns]; - for (int runIndex = 0; runIndex < numOfRuns; runIndex++) + for (var runIndex = 0; runIndex < numOfRuns; runIndex++) { string key; object value; IDictionary dictionary = new Dictionary(); - IDictionary linked = new LinkedHashMap(); + IDictionary linked = new LinkHashMap(); long dictStart = DateTime.Now.Ticks; @@ -118,12 +123,12 @@ public async Task PerformanceAsync() linked.Clear(); } - for (int runIndex = 0; runIndex < numOfRuns; runIndex++) + for (var runIndex = 0; runIndex < numOfRuns; runIndex++) { decimal linkPopulateOverhead = (linkPopulateTicks[runIndex] / (decimal)dictPopulateTicks[runIndex]); decimal linkItemOverhead = (linkItemTicks[runIndex] / (decimal)dictItemTicks[runIndex]); - string message = string.Format("LinkedHashMap vs Dictionary (Run-{0}) :",runIndex+1); + string message = string.Format("LinkHashMap vs Dictionary (Run-{0}) :",runIndex+1); message += "\n POPULATE:"; message += "\n\t linked took " + linkPopulateTicks[runIndex] + " ticks."; message += "\n\t dictionary took " + dictPopulateTicks[runIndex] + " ticks."; diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index d9e03d0df6b..5a3b08cdfc5 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -55,6 +55,7 @@ UtilityTest\AsyncReaderWriterLock.cs + UtilityTest\SetSnapShot.cs diff --git a/src/NHibernate.Test/UtilityTest/LinkHashMapFixture.cs b/src/NHibernate.Test/UtilityTest/LinkHashMapFixture.cs new file mode 100644 index 00000000000..30002757113 --- /dev/null +++ b/src/NHibernate.Test/UtilityTest/LinkHashMapFixture.cs @@ -0,0 +1,477 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using NHibernate.Util; +using NUnit.Framework; + +namespace NHibernate.Test.UtilityTest +{ + [TestFixture] + public class LinkHashMapFixture + { + private static readonly Player[] players = + { + new Player("12341", "Boeta Dippenaar"), + new Player("23432", "Gary Kirsten"), + new Player("23411", "Graeme Smith"), + new Player("55221", "Jonty Rhodes"), + new Player("61234", "Monde Zondeki"), + new Player("23415", "Paul Adams") + }; + + private static void Fill(IDictionary lhm) + { + foreach (var player in players) + lhm.Add(player.Id, player); + } + + [Test] + public void Add() + { + IDictionary lhm = new LinkHashMap(); + Fill(lhm); + lhm.Add("55555", new Player("55555", "Monde Zondeki")); + + Assert.That(lhm.Count, Is.EqualTo(7)); + } + + [Test] + public void LastKeyLastValue() + { + var lhm = new LinkHashMap(); + Fill(lhm); + Assert.That(lhm.LastKey, Is.EqualTo(players[players.Length - 1].Id)); + Assert.That(lhm.LastValue, Is.EqualTo(players[players.Length - 1])); + + // override + var antWithSameId = new Player("12341", "Another"); + lhm[antWithSameId.Id] = antWithSameId; + Assert.That(lhm.LastKey, Is.EqualTo(antWithSameId.Id)); + Assert.That(lhm.LastValue, Is.EqualTo(antWithSameId)); + } + + [Test] + public void FirstKeyFirstValue() + { + var lhm = new LinkHashMap(); + Fill(lhm); + Assert.That(lhm.FirstKey, Is.EqualTo(players[0].Id)); + Assert.That(lhm.FirstValue, Is.EqualTo(players[0])); + + // override First + var antWithSameId = new Player("12341", "Another"); + lhm[antWithSameId.Id] = antWithSameId; + Assert.That(lhm.FirstKey, Is.EqualTo(players[1].Id)); + Assert.That(lhm.FirstValue, Is.EqualTo(players[1])); + } + + [Test] + public void Clear() + { + IDictionary lhm = new LinkHashMap(); + var p = new Player("78945", "Someone"); + lhm[p.Id] = p; + + lhm.Clear(); + Assert.That(lhm, Is.Empty); + + foreach (KeyValuePair pair in lhm) + Assert.Fail("Should not be any entries but found Key = " + pair.Key + " and Value = " + pair.Value); + } + + [Test] + public void Contains() + { + var lhm = new LinkHashMap(); + Fill(lhm); + + Assert.That(lhm.Contains("12341"), Is.True); + Assert.That(lhm.Contains("55555"), Is.False); + } + + [Test] + public void GetEnumerator() + { + IDictionary lhm = new LinkHashMap(); + Fill(lhm); + int index = 0; + foreach (KeyValuePair pair in lhm) + { + Assert.That(pair.Key, Is.EqualTo(players[index].Id)); + Assert.That(pair.Value, Is.EqualTo(players[index])); + index++; + } + + Assert.That(index, Is.EqualTo(6)); + } + + [Test] + public void GetEnumeratorEmpty() + { + IDictionary lhm = new LinkHashMap(); + Assert.That(lhm, Is.Empty); + + int entries = 0; + foreach (KeyValuePair pair in lhm) + entries++; + foreach (string s in lhm.Keys) + entries++; + foreach (Player value in lhm.Values) + entries++; + + Assert.That(entries, Is.Zero, "should not have any entries in the enumerators"); + } + + [Test] + public void GetEnumeratorModifyExceptionFromAdd() + { + IDictionary lhm = new LinkHashMap(); + lhm["123"] = new Player("123", "yyyyyyy"); + Assert.That( + () => + { + foreach (var pair in lhm) + { + lhm["78945"] = new Player("78945", "Someone"); + } + }, + Throws.InvalidOperationException); + } + + [Test] + public void GetEnumeratorModifyExceptionFromRemove() + { + IDictionary lhm = new LinkHashMap(); + lhm["123"] = new Player("123", "yyyyyyy"); + Assert.That( + () => + { + foreach (var pair in lhm) + { + lhm.Remove(pair.Key); + } + }, + Throws.InvalidOperationException); + } + + [Test] + public void GetEnumeratorModifyExceptionFromUpdate() + { + IDictionary lhm = new LinkHashMap(); + lhm["123"] = new Player("123", "yyyyyyy"); + Assert.That( + () => + { + foreach (var pair in lhm) + { + lhm["123"] = new Player("123", "aaaaaaa"); + } + }, + Throws.InvalidOperationException); + } + + [Test] + public void EnumeratorInstanceShouldNotBeNonGenericIEnumerator() + { + var lhm = new LinkHashMap(); + var enumerator = lhm.GetEnumerator(); + var enumeratorType = enumerator.GetVariableType(); + + Assert.That(enumeratorType, Is.Not.EqualTo(typeof(IEnumerator))); + } + + [Test] + public void EnumeratorInstanceShouldBeStruct() + { + var lhm = new LinkHashMap(); + var enumerator = lhm.GetEnumerator(); + var enumeratorType = enumerator.GetVariableType(); + + Assert.That(enumeratorType.IsStruct, Is.True); + } + + [Test] + public void KeysEnumeratorInstanceShouldBeStruct() + { + var lhm = new LinkHashMap(); + var enumerator = lhm.Keys.GetEnumerator(); + var enumeratorType = enumerator.GetVariableType(); + + Assert.That(enumeratorType.IsStruct, Is.True); + } + + [Test] + public void ValuesEnumeratorInstanceShouldBeStruct() + { + var lhm = new LinkHashMap(); + var enumerator = lhm.Values.GetEnumerator(); + var enumeratorType = enumerator.GetVariableType(); + + Assert.That(enumeratorType.IsStruct, Is.True); + } + + [Test] + public void Remove() + { + IDictionary lhm = new LinkHashMap(); + Fill(lhm); + + // remove an item that exists + bool removed = lhm.Remove("23411"); + Assert.That(removed, Is.True); + Assert.That(lhm.Count, Is.EqualTo(5)); + + // try to remove an item that does not exist + removed = lhm.Remove("65432"); + Assert.That(removed, Is.False); + Assert.That(lhm.Count, Is.EqualTo(5)); + } + + [Test] + public void ContainsValue() + { + var lhm = new LinkHashMap(); + Fill(lhm); + Assert.That(lhm.ContainsValue(new Player("55221", "Jonty Rhodes")), Is.True); + Assert.That(lhm.ContainsValue(new Player("55221", "SameKeyDiffName")), Is.False); + } + + [Test] + public void CopyTo() + { + IDictionary lhm = new LinkHashMap(); + Fill(lhm); + var destArray = new KeyValuePair[lhm.Count + 1]; + destArray[0] = new KeyValuePair("999", new Player("999", "The number nine")); + lhm.CopyTo(destArray, 1); + + for (var i = 1; i < destArray.Length; i++) + { + Assert.That(destArray[i].Key, Is.EqualTo(players[i - 1].Id)); + Assert.That(destArray[i].Value, Is.EqualTo(players[i - 1])); + } + } + + [Test] + public void Keys() + { + IDictionary lhm = new LinkHashMap(); + Fill(lhm); + var index = 0; + foreach (string s in lhm.Keys) + { + Assert.That(s, Is.EqualTo(players[index].Id)); + index++; + } + } + + [Test] + public void Values() + { + IDictionary lhm = new LinkHashMap(); + Fill(lhm); + var index = 0; + foreach (Player p in lhm.Values) + { + Assert.That(p, Is.EqualTo(players[index])); + index++; + } + } + + [Test] + public void Serialization() + { + IDictionary lhm = new LinkHashMap(); + Fill(lhm); + + var stream = new MemoryStream(); + var f = new BinaryFormatter + { +#if !NETFX + SurrogateSelector = new SerializationHelper.SurrogateSelector() +#endif + }; + f.Serialize(stream, lhm); + stream.Position = 0; + + var dlhm = (LinkHashMap)f.Deserialize(stream); + stream.Close(); + + Assert.That(dlhm.Count, Is.EqualTo(6)); + var index = 0; + foreach (var pair in dlhm) + { + Assert.That(pair.Key, Is.EqualTo(players[index].Id)); + Assert.That(pair.Value, Is.EqualTo(players[index])); + index++; + } + + Assert.That(index, Is.EqualTo(6)); + } + + [Test, Explicit] + public void ShowDiff() + { + IDictionary dict = new Dictionary(); + IDictionary lhm = new LinkHashMap(); + Fill(dict); + Fill(lhm); + // Override the first element + var o = new Player("12341", "Override"); + dict[o.Id] = o; + lhm[o.Id] = o; + Console.Out.WriteLine("Dictionary order:"); + foreach (KeyValuePair pair in dict) + { + Console.Out.WriteLine("Key->{0}", pair.Key); + } + Console.Out.WriteLine("LinkHashMap order:"); + foreach (KeyValuePair pair in lhm) + { + Console.Out.WriteLine("Key->{0}", pair.Key); + } + } + + [Test, Explicit] + public void Performance() + { + // Take care with this test because the result is not the same every times + + int numOfRuns = 4; + + int numOfEntries = Int16.MaxValue; + + var dictPopulateTicks = new long[numOfRuns]; + var dictItemTicks = new long[numOfRuns]; + + var linkPopulateTicks = new long[numOfRuns]; + var linkItemTicks = new long[numOfRuns]; + + for (var runIndex = 0; runIndex < numOfRuns; runIndex++) + { + string key; + object value; + IDictionary dictionary = new Dictionary(); + IDictionary linked = new LinkHashMap(); + + long dictStart = DateTime.Now.Ticks; + + for (int i = 0; i < numOfEntries; i++) + { + dictionary.Add("test" + i, new object()); + } + + dictPopulateTicks[runIndex] = DateTime.Now.Ticks - dictStart; + + dictStart = DateTime.Now.Ticks; + for (int i = 0; i < numOfEntries; i++) + { + key = "test" + i; + value = dictionary[key]; + } + dictItemTicks[runIndex] = DateTime.Now.Ticks - dictStart; + + dictionary.Clear(); + + long linkStart = DateTime.Now.Ticks; + + for (int i = 0; i < numOfEntries; i++) + { + linked.Add("test" + i, new object()); + } + + linkPopulateTicks[runIndex] = DateTime.Now.Ticks - linkStart; + + linkStart = DateTime.Now.Ticks; + for (int i = 0; i < numOfEntries; i++) + { + key = "test" + i; + value = linked[key]; + } + + linkItemTicks[runIndex] = DateTime.Now.Ticks - linkStart; + + linked.Clear(); + } + + for (var runIndex = 0; runIndex < numOfRuns; runIndex++) + { + decimal linkPopulateOverhead = (linkPopulateTicks[runIndex] / (decimal)dictPopulateTicks[runIndex]); + decimal linkItemOverhead = (linkItemTicks[runIndex] / (decimal)dictItemTicks[runIndex]); + + string message = string.Format("LinkHashMap vs Dictionary (Run-{0}) :",runIndex+1); + message += "\n POPULATE:"; + message += "\n\t linked took " + linkPopulateTicks[runIndex] + " ticks."; + message += "\n\t dictionary took " + dictPopulateTicks[runIndex] + " ticks."; + message += "\n\t for an overhead of " + linkPopulateOverhead; + message += "\n RETRIVE:"; + message += "\n\t linked took " + linkItemTicks[runIndex] + " ticks."; + message += "\n\t dictionary took " + dictItemTicks[runIndex] + " ticks."; + message += "\n\t for an overhead of " + linkItemOverhead; + + Console.Out.WriteLine(message); + Console.Out.WriteLine(); + } + } + } + + internal static class LinkHashMapFixtureHelpers + { + public static System.Type GetVariableType(this T _) => typeof(T); + + public static bool IsStruct(this System.Type type) + { + return !type.IsClass && !type.IsInterface; + } + } + + [Serializable] + public class Player + { + private string id; + private string name; + + public Player(string id, string name) + { + if (string.IsNullOrEmpty(id)) + throw new ArgumentNullException(nameof(id)); + + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + this.id = id; + this.name = name; + } + + public string Id + { + get { return id; } + set { id = value; } + } + + public string Name + { + get { return name; } + set { name = value; } + } + + public override int GetHashCode() + { + return id.GetHashCode() ^ name.GetHashCode(); + } + + public override bool Equals(object obj) + { + Player that = obj as Player; + if (that == null) return false; + return id.Equals(that.id) && name.Equals(that.name); + } + + public override string ToString() + { + return $"<{id}>{name}"; + } + } +} diff --git a/src/NHibernate.Test/UtilityTest/LinkedHashMapFixture.cs b/src/NHibernate.Test/UtilityTest/LinkedHashMapFixture.cs deleted file mode 100644 index bfa995e190e..00000000000 --- a/src/NHibernate.Test/UtilityTest/LinkedHashMapFixture.cs +++ /dev/null @@ -1,415 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.Serialization.Formatters.Binary; -using NHibernate.Util; -using NUnit.Framework; - -namespace NHibernate.Test.UtilityTest -{ - [TestFixture] - public class LinkedHashMapFixture - { - private static readonly Player[] players = { - new Player("12341", "Boeta Dippenaar"), new Player("23432", "Gary Kirsten"), - new Player("23411", "Graeme Smith"), new Player("55221", "Jonty Rhodes"), - new Player("61234", "Monde Zondeki"), new Player("23415", "Paul Adams") - }; - - private static void Fill(IDictionary lhm) - { - foreach (Player player in players) - lhm.Add(player.Id, player); - } - - [Test] - public void Add() - { - IDictionary lhm = new LinkedHashMap(); - Fill(lhm); - lhm.Add("55555", new Player("55555", "Monde Zondeki")); - - Assert.AreEqual(7, lhm.Count); - } - - [Test] - public void LastKeyLastValue() - { - LinkedHashMap lhm = new LinkedHashMap(); - Fill(lhm); - Assert.AreEqual(players[players.Length - 1].Id, lhm.LastKey); - Assert.AreEqual(players[players.Length-1], lhm.LastValue); - - // override - Player antWithSameId = new Player("12341", "Another"); - lhm[antWithSameId.Id] = antWithSameId; - Assert.AreEqual(antWithSameId.Id, lhm.LastKey); - Assert.AreEqual(antWithSameId, lhm.LastValue); - } - - [Test] - public void FirstKeyFirstValue() - { - LinkedHashMap lhm = new LinkedHashMap(); - Fill(lhm); - Assert.AreEqual(players[0].Id, lhm.FirstKey); - Assert.AreEqual(players[0], lhm.FirstValue); - - // override First - Player antWithSameId = new Player("12341", "Another"); - lhm[antWithSameId.Id] = antWithSameId; - Assert.AreEqual(players[1].Id, lhm.FirstKey); - Assert.AreEqual(players[1], lhm.FirstValue); - } - - [Test] - public void Clear() - { - IDictionary lhm = new LinkedHashMap(); - Player p = new Player("78945", "Someone"); - lhm[p.Id] = p; - - lhm.Clear(); - Assert.AreEqual(0, lhm.Count); - - foreach (KeyValuePair pair in lhm) - Assert.Fail("Should not be any entries but found Key = " + pair.Key + " and Value = " + pair.Value); - } - - [Test] - public void Contains() - { - LinkedHashMap lhm = new LinkedHashMap(); - Fill(lhm); - - Assert.IsTrue(lhm.Contains("12341")); - Assert.IsFalse(lhm.Contains("55555")); - } - - [Test] - public void GetEnumerator() - { - IDictionary lhm = new LinkedHashMap(); - Fill(lhm); - int index = 0; - foreach (KeyValuePair pair in lhm) - { - Assert.AreEqual(players[index].Id, pair.Key); - Assert.AreEqual(players[index], pair.Value); - index++; - } - - Assert.AreEqual(6, index); - } - - [Test] - public void GetEnumeratorEmpty() - { - IDictionary lhm = new LinkedHashMap(); - Assert.AreEqual(0, lhm.Count); - - int entries = 0; - foreach (KeyValuePair pair in lhm) - entries++; - foreach (string s in lhm.Keys) - entries++; - foreach (Player value in lhm.Values) - entries++; - - Assert.AreEqual(0, entries, "should not have any entries in the enumerators"); - } - - [Test] - public void GetEnumeratorModifyExceptionFromAdd() - { - IDictionary lhm = new LinkedHashMap(); - lhm["123"] = new Player("123", "yyyyyyy"); - Assert.Throws(() => - { - foreach (KeyValuePair pair in lhm) - { - lhm["78945"] = new Player("78945", "Someone"); - } - }); - } - - [Test] - public void GetEnumeratorModifyExceptionFromRemove() - { - IDictionary lhm = new LinkedHashMap(); - lhm["123"] = new Player("123", "yyyyyyy"); - Assert.Throws(() => - { - foreach (KeyValuePair pair in lhm) - { - lhm.Remove(pair.Key); - } - }); - } - - [Test] - public void GetEnumeratorModifyExceptionFromUpdate() - { - IDictionary lhm = new LinkedHashMap(); - lhm["123"] = new Player("123", "yyyyyyy"); - Assert.Throws(() => - { - foreach (KeyValuePair pair in lhm) - { - lhm["123"] = new Player("123", "aaaaaaa"); - } - }); - } - - [Test] - public void Remove() - { - IDictionary lhm = new LinkedHashMap(); - Fill(lhm); - - // remove an item that exists - bool removed =lhm.Remove("23411"); - Assert.IsTrue(removed); - Assert.AreEqual(5, lhm.Count); - - // try to remove an item that does not exist - removed= lhm.Remove("65432"); - Assert.IsFalse(removed); - Assert.AreEqual(5, lhm.Count); - } - - [Test] - public void ContainsValue() - { - LinkedHashMap lhm = new LinkedHashMap(); - Fill(lhm); - Assert.IsTrue(lhm.ContainsValue(new Player("55221", "Jonty Rhodes"))); - Assert.IsFalse(lhm.ContainsValue(new Player("55221", "SameKeyDiffName"))); - } - - [Test] - public void CopyTo() - { - IDictionary lhm = new LinkedHashMap(); - Fill(lhm); - KeyValuePair[] destArray = new KeyValuePair[lhm.Count + 1]; - destArray[0] = new KeyValuePair("999", new Player("999", "The number nine")); - lhm.CopyTo(destArray, 1); - - for (int i = 1; i < destArray.Length; i++) - { - Assert.AreEqual(players[i-1].Id, destArray[i].Key); - Assert.AreEqual(players[i-1], destArray[i].Value); - } - } - - [Test] - public void Keys() - { - IDictionary lhm = new LinkedHashMap(); - Fill(lhm); - int index = 0; - foreach (string s in lhm.Keys) - { - Assert.AreEqual(players[index].Id, s); - index++; - } - } - - [Test] - public void Values() - { - IDictionary lhm = new LinkedHashMap(); - Fill(lhm); - int index = 0; - foreach (Player p in lhm.Values) - { - Assert.AreEqual(players[index], p); - index++; - } - } - - [Test] - public void Serialization() - { - IDictionary lhm = new LinkedHashMap(); - Fill(lhm); - - MemoryStream stream = new MemoryStream(); - var f = new BinaryFormatter - { -#if !NETFX - SurrogateSelector = new SerializationHelper.SurrogateSelector() -#endif - }; - f.Serialize(stream, lhm); - stream.Position = 0; - - LinkedHashMap dlhm = (LinkedHashMap)f.Deserialize(stream); - stream.Close(); - - Assert.AreEqual(6, dlhm.Count); - int index = 0; - foreach (KeyValuePair pair in dlhm) - { - Assert.AreEqual(players[index].Id, pair.Key); - Assert.AreEqual(players[index], pair.Value); - index++; - } - - Assert.AreEqual(6, index); - } - - [Test, Explicit] - public void ShowDiff() - { - IDictionary dict = new Dictionary(); - IDictionary lhm = new LinkedHashMap(); - Fill(dict); - Fill(lhm); - // Override the first element - Player o = new Player("12341", "Ovirride"); - dict[o.Id] = o; - lhm[o.Id] = o; - Console.Out.WriteLine("Dictionary order:"); - foreach (KeyValuePair pair in dict) - { - Console.Out.WriteLine("Key->{0}", pair.Key); - } - Console.Out.WriteLine("LinkedHashMap order:"); - foreach (KeyValuePair pair in lhm) - { - Console.Out.WriteLine("Key->{0}", pair.Key); - } - } - - [Test, Explicit] - public void Performance() - { - // Take care with this test because the result is not the same every times - - int numOfRuns = 4; - - int numOfEntries = Int16.MaxValue; - - long[] dictPopulateTicks = new long[numOfRuns]; - long[] dictItemTicks = new long[numOfRuns]; - - long[] linkPopulateTicks = new long[numOfRuns]; - long[] linkItemTicks = new long[numOfRuns]; - - for (int runIndex = 0; runIndex < numOfRuns; runIndex++) - { - string key; - object value; - IDictionary dictionary = new Dictionary(); - IDictionary linked = new LinkedHashMap(); - - long dictStart = DateTime.Now.Ticks; - - for (int i = 0; i < numOfEntries; i++) - { - dictionary.Add("test" + i, new object()); - } - - dictPopulateTicks[runIndex] = DateTime.Now.Ticks - dictStart; - - dictStart = DateTime.Now.Ticks; - for (int i = 0; i < numOfEntries; i++) - { - key = "test" + i; - value = dictionary[key]; - } - dictItemTicks[runIndex] = DateTime.Now.Ticks - dictStart; - - dictionary.Clear(); - - long linkStart = DateTime.Now.Ticks; - - for (int i = 0; i < numOfEntries; i++) - { - linked.Add("test" + i, new object()); - } - - linkPopulateTicks[runIndex] = DateTime.Now.Ticks - linkStart; - - linkStart = DateTime.Now.Ticks; - for (int i = 0; i < numOfEntries; i++) - { - key = "test" + i; - value = linked[key]; - } - - linkItemTicks[runIndex] = DateTime.Now.Ticks - linkStart; - - linked.Clear(); - } - - for (int runIndex = 0; runIndex < numOfRuns; runIndex++) - { - decimal linkPopulateOverhead = (linkPopulateTicks[runIndex] / (decimal)dictPopulateTicks[runIndex]); - decimal linkItemOverhead = (linkItemTicks[runIndex] / (decimal)dictItemTicks[runIndex]); - - string message = string.Format("LinkedHashMap vs Dictionary (Run-{0}) :",runIndex+1); - message += "\n POPULATE:"; - message += "\n\t linked took " + linkPopulateTicks[runIndex] + " ticks."; - message += "\n\t dictionary took " + dictPopulateTicks[runIndex] + " ticks."; - message += "\n\t for an overhead of " + linkPopulateOverhead; - message += "\n RETRIVE:"; - message += "\n\t linked took " + linkItemTicks[runIndex] + " ticks."; - message += "\n\t dictionary took " + dictItemTicks[runIndex] + " ticks."; - message += "\n\t for an overhead of " + linkItemOverhead; - - Console.Out.WriteLine(message); - Console.Out.WriteLine(); - } - } - } - - [Serializable] - public class Player - { - private string id; - private string name; - public Player(string id, string name) - { - if (string.IsNullOrEmpty(id)) - throw new ArgumentNullException("id"); - - if (string.IsNullOrEmpty(name)) - throw new ArgumentNullException("name"); - - this.id = id; - this.name = name; - } - - public string Id - { - get { return id; } - set { id = value; } - } - - public string Name - { - get { return name; } - set { name = value; } - } - - public override int GetHashCode() - { - return id.GetHashCode() ^ name.GetHashCode(); - } - - public override bool Equals(object obj) - { - Player that = obj as Player; - if(that==null) return false; - return id.Equals(that.id) && name.Equals(that.name); - } - - public override string ToString() - { - return string.Format("<{0}>{1}", id, name); - } - } -} diff --git a/src/NHibernate/Async/Engine/BatchFetchQueue.cs b/src/NHibernate/Async/Engine/BatchFetchQueue.cs index 4500f7eca55..9f7cf11e586 100644 --- a/src/NHibernate/Async/Engine/BatchFetchQueue.cs +++ b/src/NHibernate/Async/Engine/BatchFetchQueue.cs @@ -72,7 +72,7 @@ internal async Task GetCollectionBatchAsync(ICollectionPersister colle return keys; } - foreach (KeyValuePair me in map) + foreach (var me in map) { cancellationToken.ThrowIfCancellationRequested(); if (ProcessKey(me) ?? await (CheckCacheAndProcessResultAsync()).ConfigureAwait(false)) diff --git a/src/NHibernate/Cfg/XmlHbmBinding/NamedQueryBinder.cs b/src/NHibernate/Cfg/XmlHbmBinding/NamedQueryBinder.cs index b008b52d4f5..63e15fbc61e 100644 --- a/src/NHibernate/Cfg/XmlHbmBinding/NamedQueryBinder.cs +++ b/src/NHibernate/Cfg/XmlHbmBinding/NamedQueryBinder.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using NHibernate.Cfg.MappingSchema; using NHibernate.Engine; using NHibernate.Util; @@ -31,7 +30,7 @@ public void AddQuery(HbmQuery querySchema) ? querySchema.cachemode.ToCacheMode() : null; - IDictionary parameterTypes = new LinkedHashMap(); + var parameterTypes = new LinkHashMap(); var namedQuery = new NamedQueryDefinition(queryText, cacheable, region, timeout, fetchSize, flushMode, cacheMode, readOnly, comment, parameterTypes); @@ -39,4 +38,4 @@ public void AddQuery(HbmQuery querySchema) mappings.AddQuery(queryName, namedQuery); } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Engine/BatchFetchQueue.cs b/src/NHibernate/Engine/BatchFetchQueue.cs index d9bc7068108..3e5bb08d576 100644 --- a/src/NHibernate/Engine/BatchFetchQueue.cs +++ b/src/NHibernate/Engine/BatchFetchQueue.cs @@ -31,7 +31,7 @@ public partial class BatchFetchQueue /// private readonly Dictionary subselectsByEntityKey = new Dictionary(8); - private readonly Dictionary> batchLoadableCollections = new Dictionary>(8); + private readonly Dictionary> batchLoadableCollections = new(8); /// /// The owning persistence context. /// @@ -164,7 +164,7 @@ public void AddBatchLoadableCollection(IPersistentCollection collection, Collect if (!batchLoadableCollections.TryGetValue(persister.Role, out var map)) { - map = new LinkedHashMap(); + map = new LinkHashMap(); batchLoadableCollections.Add(persister.Role, map); } map[ce] = collection; @@ -245,7 +245,7 @@ internal object[] GetCollectionBatch(ICollectionPersister collectionPersister, o return keys; } - foreach (KeyValuePair me in map) + foreach (var me in map) { if (ProcessKey(me) ?? CheckCacheAndProcessResult()) { diff --git a/src/NHibernate/Mapping/Table.cs b/src/NHibernate/Mapping/Table.cs index 3082a9c02b0..5311ece174d 100644 --- a/src/NHibernate/Mapping/Table.cs +++ b/src/NHibernate/Mapping/Table.cs @@ -27,7 +27,7 @@ public enum SchemaAction public class Table : IRelationalModel { private readonly List checkConstraints = new List(); - private readonly LinkedHashMap columns = new LinkedHashMap(); + private readonly LinkHashMap columns = new(); private readonly Dictionary foreignKeys = new Dictionary(); private readonly Dictionary indexes = new Dictionary(); private int? uniqueInteger; diff --git a/src/NHibernate/SqlCommand/CaseFragment.cs b/src/NHibernate/SqlCommand/CaseFragment.cs index 5a4a0ff3c1f..a7d1240cc60 100644 --- a/src/NHibernate/SqlCommand/CaseFragment.cs +++ b/src/NHibernate/SqlCommand/CaseFragment.cs @@ -9,7 +9,7 @@ public abstract class CaseFragment protected internal readonly Dialect.Dialect dialect; protected internal string returnColumnName; - protected internal IDictionary cases = new LinkedHashMap(); + protected internal IDictionary cases = new LinkHashMap(); protected CaseFragment(Dialect.Dialect dialect) { @@ -35,4 +35,4 @@ public virtual CaseFragment AddWhenColumnNotNull(string alias, string columnName public abstract string ToSqlStringFragment(); } -} \ No newline at end of file +} diff --git a/src/NHibernate/SqlCommand/SqlInsertBuilder.cs b/src/NHibernate/SqlCommand/SqlInsertBuilder.cs index d73f98fd4d5..0a8f88883f8 100644 --- a/src/NHibernate/SqlCommand/SqlInsertBuilder.cs +++ b/src/NHibernate/SqlCommand/SqlInsertBuilder.cs @@ -19,7 +19,7 @@ public class SqlInsertBuilder : ISqlStringBuilder private string comment; // columns-> (ColumnName, Value) or (ColumnName, SqlType) for parametrized column - private readonly LinkedHashMap columns = new LinkedHashMap(); + private readonly LinkHashMap columns = new(); public SqlInsertBuilder(ISessionFactoryImplementor factory) { diff --git a/src/NHibernate/SqlCommand/SqlUpdateBuilder.cs b/src/NHibernate/SqlCommand/SqlUpdateBuilder.cs index c9b7ccd1352..80c154ffdf8 100644 --- a/src/NHibernate/SqlCommand/SqlUpdateBuilder.cs +++ b/src/NHibernate/SqlCommand/SqlUpdateBuilder.cs @@ -19,7 +19,7 @@ public class SqlUpdateBuilder : SqlBaseBuilder, ISqlStringBuilder private string comment; // columns-> (ColumnName, Value) or (ColumnName, SqlType) for parametrized column - private readonly LinkedHashMap columns = new LinkedHashMap(); + private readonly LinkHashMap columns = new(); private List whereStrings = new List(); private readonly List whereParameterTypes = new List(); diff --git a/src/NHibernate/Util/CollectionHelper.cs b/src/NHibernate/Util/CollectionHelper.cs index e6ed9842065..6f40567923c 100644 --- a/src/NHibernate/Util/CollectionHelper.cs +++ b/src/NHibernate/Util/CollectionHelper.cs @@ -733,8 +733,9 @@ public static bool DictionaryEquals(IDictionary m1, IDictionary.Default) : m1.All(kv => m2.TryGetValue(kv.Key, out var value) && comparer.Equals(kv.Value, value))); +#if !NETCOREAPP2_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER //It's added to make use of optimized .NET Core Dictionary.Remove(key, out value) method - internal static bool Remove(this Dictionary dic, TKey key, out TValue value) + public static bool Remove(this Dictionary dic, TKey key, out TValue value) { if (!dic.TryGetValue(key, out value)) return false; @@ -742,6 +743,7 @@ internal static bool Remove(this Dictionary dic, TKe dic.Remove(key); return true; } +#endif private static bool? FastCheckEquality(IEnumerable c1, IEnumerable c2) { diff --git a/src/NHibernate/Util/LinkHashMap.cs b/src/NHibernate/Util/LinkHashMap.cs new file mode 100644 index 00000000000..26d3e147506 --- /dev/null +++ b/src/NHibernate/Util/LinkHashMap.cs @@ -0,0 +1,624 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Serialization; +using System.Text; +using NHibernate.DebugHelpers; + +namespace NHibernate.Util +{ + /// + /// A map of objects whose mapping entries are sequenced based on the order in which they were + /// added. This data structure has fast O(1) search time, deletion time, and insertion time + /// + /// + /// This class is not thread safe. + /// This class is not really a replication of JDK LinkedHashMap{K, V}, + /// this class is an adaptation of SequencedHashMap with generics. + /// + [DebuggerTypeProxy(typeof(CollectionProxy<>))] + [Serializable] + internal class LinkHashMap : IDictionary, IDeserializationCallback + { + [Serializable] + protected class Entry + { + public Entry(TKey key, TValue value) + { + Key = key; + Value = value; + } + + public TKey Key { get; } + + public TValue Value { get; set; } + + public Entry Next { get; set; } + + public Entry Prev { get; set; } + + #region System.Object Members + + public override int GetHashCode() + { + return Key == null ? 0 : Key.GetHashCode(); + } + + public override bool Equals(object obj) + { + var other = obj as Entry; + if (other == null) return false; + if (other == this) return true; + + return (Key == null ? other.Key == null : Key.Equals(other.Key)) && + (Value == null ? other.Value == null : Value.Equals(other.Value)); + } + + public override string ToString() + { + return "[" + Key + "=" + Value + "]"; + } + + #endregion + } + + private readonly Entry _header; + private readonly Dictionary _entries; + private long _version; + + /// + /// Initializes a new instance of the class that is empty, + /// has the default initial capacity, and uses the default equality comparer for the key type. + /// + public LinkHashMap() + : this(0, null) + { + } + + /// + /// Initializes a new instance of the class that is empty, + /// has the specified initial capacity, and uses the default equality comparer for the key type. + /// + /// The initial number of elements that the can contain. + public LinkHashMap(int capacity) + : this(capacity, null) + { + } + + /// + /// Initializes a new instance of the class that is empty, has the default initial capacity, and uses the specified . + /// + /// The implementation to use when comparing keys, or null to use the default EqualityComparer for the type of the key. + public LinkHashMap(IEqualityComparer equalityComparer) + : this(0, equalityComparer) + { + } + + /// + /// Initializes a new instance of the class that is empty, has the specified initial capacity, and uses the specified . + /// + /// The initial number of elements that the can contain. + /// The implementation to use when comparing keys, or null to use the default EqualityComparer for the type of the key. + public LinkHashMap(int capacity, IEqualityComparer equalityComparer) + { + _header = CreateSentinel(); + _entries = new Dictionary(capacity, equalityComparer); + } + + #region IDictionary Members + + public virtual bool ContainsKey(TKey key) + { + return _entries.ContainsKey(key); + } + + public virtual void Add(TKey key, TValue value) + { + var e = new Entry(key, value); + _entries.Add(key, e); + _version++; + InsertEntry(e); + } + + public virtual bool Remove(TKey key) + { + return RemoveImpl(key); + } + + public bool TryGetValue(TKey key, out TValue value) + { + var result = _entries.TryGetValue(key, out var entry); + if (result) + value = entry.Value; + else + value = default; + + return result; + } + + public TValue this[TKey key] + { + get + { + return _entries[key].Value; + } + set + { + if (_entries.TryGetValue(key, out var e)) + OverrideEntry(e, value); + else + Add(key, value); + } + } + + private void OverrideEntry(Entry e, TValue value) + { + _version++; + RemoveEntry(e); + e.Value = value; + InsertEntry(e); + } + + public KeyCollection Keys => new KeyCollection(this); + + ICollection IDictionary.Keys => new KeyCollection(this); + + public virtual ValueCollection Values => new ValueCollection(this); + + ICollection IDictionary.Values => new ValueCollection(this); + + #endregion + + #region ICollection> Members + + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public virtual void Clear() + { + _version++; + + _entries.Clear(); + + _header.Next = _header; + _header.Prev = _header; + } + + public bool Contains(KeyValuePair item) + { + return Contains(item.Key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var pair in this) + array.SetValue(pair, arrayIndex++); + } + + public bool Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + public virtual int Count => _entries.Count; + + public virtual bool IsReadOnly => false; + + #endregion + + public Enumerator GetEnumerator() => new Enumerator(this); + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(this); + } + + #endregion + + #region IEnumerable> Members + + IEnumerator> IEnumerable>.GetEnumerator() + { + return new Enumerator(this); + } + + #endregion + + #region LinkHashMap Members + + private bool IsEmpty + { + get { return _header.Next == _header; } + } + + public virtual bool IsFixedSize => false; + + public virtual TKey FirstKey + { + get { return First == null ? default : First.Key; } + } + + public virtual TValue FirstValue + { + get { return First == null ? default : First.Value; } + } + + public virtual TKey LastKey + { + get { return Last == null ? default : Last.Key; } + } + + public virtual TValue LastValue + { + get { return Last == null ? default : Last.Value; } + } + + public virtual bool Contains(TKey key) + { + return ContainsKey(key); + } + + public virtual bool ContainsValue(TValue value) + { + if (value == null) + { + for (var entry = _header.Next; entry != _header; entry = entry.Next) + { + if (entry.Value == null) return true; + } + } + else + { + for (var entry = _header.Next; entry != _header; entry = entry.Next) + { + if (value.Equals(entry.Value)) return true; + } + } + return false; + } + + #endregion + + private static Entry CreateSentinel() + { + var s = new Entry(default, default); + s.Prev = s; + s.Next = s; + return s; + } + + private static void RemoveEntry(Entry entry) + { + entry.Next.Prev = entry.Prev; + entry.Prev.Next = entry.Next; + } + + private void InsertEntry(Entry entry) + { + entry.Next = _header; + entry.Prev = _header.Prev; + _header.Prev.Next = entry; + _header.Prev = entry; + } + + private Entry First + { + get { return IsEmpty ? null : _header.Next; } + } + + private Entry Last + { + get { return IsEmpty ? null : _header.Prev; } + } + + private bool RemoveImpl(TKey key) + { + if (!_entries.Remove(key, out var e)) + return false; + + _version++; + RemoveEntry(e); + return true; + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + ((IDeserializationCallback)_entries).OnDeserialization(sender); + } + + #region System.Object Members + + public override string ToString() + { + var buf = new StringBuilder(); + buf.Append('['); + for (Entry pos = _header.Next; pos != _header; pos = pos.Next) + { + buf.Append(pos.Key); + buf.Append('='); + buf.Append(pos.Value); + if (pos.Next != _header) + { + buf.Append(','); + } + } + buf.Append(']'); + + return buf.ToString(); + } + + #endregion + + public class KeyCollection : ICollection + { + private readonly LinkHashMap _dictionary; + + public KeyCollection(LinkHashMap dictionary) + { + _dictionary = dictionary; + } + + #region ICollection Members + + void ICollection.Add(TKey item) + { + throw new NotSupportedException($"{nameof(LinkHashMap)}+{nameof(KeyCollection)} is readonly."); + } + + void ICollection.Clear() + { + throw new NotSupportedException($"{nameof(LinkHashMap)}+{nameof(KeyCollection)} is readonly."); + } + + bool ICollection.Contains(TKey item) + { + foreach (var key in this) + { + if (key.Equals(item)) + return true; + } + return false; + } + + public void CopyTo(TKey[] array, int arrayIndex) + { + foreach (var key in this) + array.SetValue(key, arrayIndex++); + } + + bool ICollection.Remove(TKey item) + { + throw new NotSupportedException($"{nameof(LinkHashMap)}+{nameof(KeyCollection)} is readonly."); + } + + public int Count => _dictionary.Count; + + bool ICollection.IsReadOnly => true; + + #endregion + + public Enumerator GetEnumerator() => new(_dictionary); + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_dictionary); + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_dictionary); + + #endregion + + public struct Enumerator : IEnumerator + { + private readonly LinkHashMap _dictionary; + private Entry _current; + private readonly long _version; + + public Enumerator(LinkHashMap dictionary) + { + _dictionary = dictionary; + _version = dictionary._version; + _current = dictionary._header; + } + + public bool MoveNext() + { + if (_dictionary._version != _version) + throw new InvalidOperationException("Enumerator was modified"); + + if (_current.Next == _dictionary._header) + return false; + + _current = _current.Next; + + return true; + } + + public TKey Current + { + get + { + if (_dictionary._version != _version) + throw new InvalidOperationException("Enumerator was modified"); + + return _current.Key; + } + } + + object IEnumerator.Current => Current; + + void IEnumerator.Reset() + { + _current = _dictionary._header; + } + + void IDisposable.Dispose() { } + } + } + + public class ValueCollection : ICollection + { + private readonly LinkHashMap _dictionary; + + public ValueCollection(LinkHashMap dictionary) + { + _dictionary = dictionary; + } + + #region ICollection Members + + void ICollection.Add(TValue item) + { + throw new NotSupportedException($"{nameof(LinkHashMap)}+{nameof(ValueCollection)} is readonly."); + } + + void ICollection.Clear() + { + throw new NotSupportedException($"{nameof(LinkHashMap)}+{nameof(ValueCollection)} is readonly."); + } + + bool ICollection.Contains(TValue item) + { + foreach (var value in this) + { + if (value.Equals(item)) + return true; + } + return false; + } + + public void CopyTo(TValue[] array, int arrayIndex) + { + foreach (var value in this) + array.SetValue(value, arrayIndex++); + } + + bool ICollection.Remove(TValue item) + { + throw new NotSupportedException($"{nameof(LinkHashMap)}+{nameof(ValueCollection)} is readonly."); + } + + public int Count => _dictionary.Count; + + bool ICollection.IsReadOnly => true; + + #endregion + + public Enumerator GetEnumerator() => new Enumerator(_dictionary); + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_dictionary); + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_dictionary); + + #endregion + + public struct Enumerator : IEnumerator + { + private readonly LinkHashMap _dictionary; + private Entry _current; + private readonly long _version; + + public Enumerator(LinkHashMap dictionary) + { + _dictionary = dictionary; + _version = dictionary._version; + _current = dictionary._header; + } + + public bool MoveNext() + { + if (_dictionary._version != _version) + throw new InvalidOperationException("Enumerator was modified"); + + if (_current.Next == _dictionary._header) + return false; + + _current = _current.Next; + + return true; + } + + public TValue Current + { + get + { + if (_dictionary._version != _version) + throw new InvalidOperationException("Enumerator was modified"); + + return _current.Value; + } + } + + object IEnumerator.Current => Current; + + void IEnumerator.Reset() + { + _current = _dictionary._header; + } + + void IDisposable.Dispose() { } + } + } + + public struct Enumerator : IEnumerator> + { + private readonly LinkHashMap _dictionary; + private Entry _current; + private readonly long _version; + + public Enumerator(LinkHashMap dictionary) + { + _dictionary = dictionary; + _version = dictionary._version; + _current = dictionary._header; + } + + public bool MoveNext() + { + if (_dictionary._version != _version) + throw new InvalidOperationException("Enumerator was modified"); + + if (_current.Next == _dictionary._header) + return false; + + _current = _current.Next; + + return true; + } + + public KeyValuePair Current + { + get + { + if (_dictionary._version != _version) + throw new InvalidOperationException("Enumerator was modified"); + + return new KeyValuePair(_current.Key, _current.Value); + } + } + + object IEnumerator.Current => Current; + + void IEnumerator.Reset() + { + _current = _dictionary._header; + } + + void IDisposable.Dispose() { } + } + } +} diff --git a/src/NHibernate/Util/LinkedHashMap.cs b/src/NHibernate/Util/LinkedHashMap.cs index 5487204deee..e2ce34b60f2 100644 --- a/src/NHibernate/Util/LinkedHashMap.cs +++ b/src/NHibernate/Util/LinkedHashMap.cs @@ -19,6 +19,8 @@ namespace NHibernate.Util /// [DebuggerTypeProxy(typeof(CollectionProxy<>))] [Serializable] + // Since 5.6 + [Obsolete("This class has no more usages and will be removed in a future version.")] public class LinkedHashMap : IDictionary, IDeserializationCallback { [Serializable]