diff --git a/src/Markdig.Tests/TestMarkdigCoreApi.cs b/src/Markdig.Tests/TestMarkdigCoreApi.cs index 415046646..ab26d4137 100644 --- a/src/Markdig.Tests/TestMarkdigCoreApi.cs +++ b/src/Markdig.Tests/TestMarkdigCoreApi.cs @@ -11,11 +11,14 @@ public class TestMarkdigCoreApi [Test] public void TestToHtml() { - string html = Markdown.ToHtml("This is a text with some *emphasis*"); - Assert.AreEqual("

This is a text with some emphasis

\n", html); - - html = Markdown.ToHtml("This is a text with a https://link.tld/"); - Assert.AreNotEqual("

This is a text with a https://link.tld/

\n", html); + for (int i = 0; i < 5; i++) + { + string html = Markdown.ToHtml("This is a text with some *emphasis*"); + Assert.AreEqual("

This is a text with some emphasis

\n", html); + + html = Markdown.ToHtml("This is a text with a https://link.tld/"); + Assert.AreNotEqual("

This is a text with a https://link.tld/

\n", html); + } } [Test] @@ -24,49 +27,66 @@ public void TestToHtmlWithPipeline() var pipeline = new MarkdownPipelineBuilder() .Build(); - string html = Markdown.ToHtml("This is a text with some *emphasis*", pipeline); - Assert.AreEqual("

This is a text with some emphasis

\n", html); + for (int i = 0; i < 5; i++) + { + string html = Markdown.ToHtml("This is a text with some *emphasis*", pipeline); + Assert.AreEqual("

This is a text with some emphasis

\n", html); - html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline); - Assert.AreNotEqual("

This is a text with a https://link.tld/

\n", html); + html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline); + Assert.AreNotEqual("

This is a text with a https://link.tld/

\n", html); + } pipeline = new MarkdownPipelineBuilder() .UseAdvancedExtensions() .Build(); - html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline); - Assert.AreEqual("

This is a text with a https://link.tld/

\n", html); + for (int i = 0; i < 5; i++) + { + string html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline); + Assert.AreEqual("

This is a text with a https://link.tld/

\n", html); + } } [Test] public void TestToHtmlWithWriter() { - StringWriter writer = new StringWriter(); + var writer = new StringWriter(); - _ = Markdown.ToHtml("This is a text with some *emphasis*", writer); - string html = writer.ToString(); - Assert.AreEqual("

This is a text with some emphasis

\n", html); + for (int i = 0; i < 5; i++) + { + _ = Markdown.ToHtml("This is a text with some *emphasis*", writer); + string html = writer.ToString(); + Assert.AreEqual("

This is a text with some emphasis

\n", html); + writer.GetStringBuilder().Length = 0; + } writer = new StringWriter(); var pipeline = new MarkdownPipelineBuilder() .UseAdvancedExtensions() .Build(); - _ = Markdown.ToHtml("This is a text with a https://link.tld/", writer, pipeline); - html = writer.ToString(); - Assert.AreEqual("

This is a text with a https://link.tld/

\n", html); - + for (int i = 0; i < 5; i++) + { + _ = Markdown.ToHtml("This is a text with a https://link.tld/", writer, pipeline); + string html = writer.ToString(); + Assert.AreEqual("

This is a text with a https://link.tld/

\n", html); + writer.GetStringBuilder().Length = 0; + } } [Test] public void TestConvert() { - StringWriter writer = new StringWriter(); - HtmlRenderer renderer = new HtmlRenderer(writer); + var writer = new StringWriter(); + var renderer = new HtmlRenderer(writer); - _ = Markdown.Convert("This is a text with some *emphasis*", renderer); - string html = writer.ToString(); - Assert.AreEqual("

This is a text with some emphasis

\n", html); + for (int i = 0; i < 5; i++) + { + _ = Markdown.Convert("This is a text with some *emphasis*", renderer); + string html = writer.ToString(); + Assert.AreEqual("

This is a text with some emphasis

\n", html); + writer.GetStringBuilder().Length = 0; + } writer = new StringWriter(); renderer = new HtmlRenderer(writer); @@ -74,9 +94,13 @@ public void TestConvert() .UseAdvancedExtensions() .Build(); - _ = Markdown.Convert("This is a text with a https://link.tld/", renderer, pipeline); - html = writer.ToString(); - Assert.AreEqual("

This is a text with a https://link.tld/

\n", html); + for (int i = 0; i < 5; i++) + { + _ = Markdown.Convert("This is a text with a https://link.tld/", renderer, pipeline); + string html = writer.ToString(); + Assert.AreEqual("

This is a text with a https://link.tld/

\n", html); + writer.GetStringBuilder().Length = 0; + } } [Test] @@ -88,62 +112,77 @@ public void TestParse() .UsePreciseSourceLocation() .Build(); - MarkdownDocument document = Markdown.Parse(markdown, pipeline); - - Assert.AreEqual(1, document.LineCount); - Assert.AreEqual(markdown.Length, document.Span.Length); - Assert.AreEqual(1, document.LineStartIndexes.Count); - Assert.AreEqual(0, document.LineStartIndexes[0]); - - Assert.AreEqual(1, document.Count); - ParagraphBlock paragraph = document[0] as ParagraphBlock; - Assert.NotNull(paragraph); - Assert.AreEqual(markdown.Length, paragraph.Span.Length); - LiteralInline literal = paragraph.Inline.FirstChild as LiteralInline; - Assert.NotNull(literal); - Assert.AreEqual("This is a text with some ", literal.ToString()); - EmphasisInline emphasis = literal.NextSibling as EmphasisInline; - Assert.NotNull(emphasis); - Assert.AreEqual("*emphasis*".Length, emphasis.Span.Length); - LiteralInline emphasisLiteral = emphasis.FirstChild as LiteralInline; - Assert.NotNull(emphasisLiteral); - Assert.AreEqual("emphasis", emphasisLiteral.ToString()); - Assert.Null(emphasisLiteral.NextSibling); - Assert.Null(emphasis.NextSibling); + for (int i = 0; i < 5; i++) + { + MarkdownDocument document = Markdown.Parse(markdown, pipeline); + + Assert.AreEqual(1, document.LineCount); + Assert.AreEqual(markdown.Length, document.Span.Length); + Assert.AreEqual(1, document.LineStartIndexes.Count); + Assert.AreEqual(0, document.LineStartIndexes[0]); + + Assert.AreEqual(1, document.Count); + ParagraphBlock paragraph = document[0] as ParagraphBlock; + Assert.NotNull(paragraph); + Assert.AreEqual(markdown.Length, paragraph.Span.Length); + LiteralInline literal = paragraph.Inline.FirstChild as LiteralInline; + Assert.NotNull(literal); + Assert.AreEqual("This is a text with some ", literal.ToString()); + EmphasisInline emphasis = literal.NextSibling as EmphasisInline; + Assert.NotNull(emphasis); + Assert.AreEqual("*emphasis*".Length, emphasis.Span.Length); + LiteralInline emphasisLiteral = emphasis.FirstChild as LiteralInline; + Assert.NotNull(emphasisLiteral); + Assert.AreEqual("emphasis", emphasisLiteral.ToString()); + Assert.Null(emphasisLiteral.NextSibling); + Assert.Null(emphasis.NextSibling); + } } [Test] public void TestNormalize() { - string normalized = Markdown.Normalize("Heading\n======="); - Assert.AreEqual("# Heading", normalized); + for (int i = 0; i < 5; i++) + { + string normalized = Markdown.Normalize("Heading\n======="); + Assert.AreEqual("# Heading", normalized); + } } [Test] public void TestNormalizeWithWriter() { - StringWriter writer = new StringWriter(); - - _ = Markdown.Normalize("Heading\n=======", writer); - string normalized = writer.ToString(); - Assert.AreEqual("# Heading", normalized); + for (int i = 0; i < 5; i++) + { + var writer = new StringWriter(); + + _ = Markdown.Normalize("Heading\n=======", writer); + string normalized = writer.ToString(); + Assert.AreEqual("# Heading", normalized); + } } [Test] public void TestToPlainText() { - string plainText = Markdown.ToPlainText("*Hello*, [world](http://example.com)!"); - Assert.AreEqual("Hello, world!\n", plainText); + for (int i = 0; i < 5; i++) + { + string plainText = Markdown.ToPlainText("*Hello*, [world](http://example.com)!"); + Assert.AreEqual("Hello, world!\n", plainText); + } } [Test] public void TestToPlainTextWithWriter() { - StringWriter writer = new StringWriter(); - - _ = Markdown.ToPlainText("*Hello*, [world](http://example.com)!", writer); - string plainText = writer.ToString(); - Assert.AreEqual("Hello, world!\n", plainText); + for (int i = 0; i < 5; i++) + { + var writer = new StringWriter(); + + _ = Markdown.ToPlainText("*Hello*, [world](http://example.com)!", writer); + string plainText = writer.ToString(); + Assert.AreEqual("Hello, world!\n", plainText); + } } } } diff --git a/src/Markdig/Markdown.cs b/src/Markdig/Markdown.cs index 3c04abbee..f31bf760f 100644 --- a/src/Markdig/Markdown.cs +++ b/src/Markdig/Markdown.cs @@ -23,6 +23,25 @@ public static partial class Markdown { public static readonly string Version = ((AssemblyFileVersionAttribute) typeof(Markdown).Assembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false)[0]).Version; + private static readonly MarkdownPipeline _defaultPipeline = new MarkdownPipelineBuilder().Build(); + private static readonly MarkdownPipeline _defaultTrackTriviaPipeline = new MarkdownPipelineBuilder().EnableTrackTrivia().Build(); + + private static MarkdownPipeline GetPipeline(MarkdownPipeline? pipeline, string markdown) + { + if (pipeline is null) + { + return _defaultPipeline; + } + + var selfPipeline = pipeline.Extensions.Find(); + if (selfPipeline is not null) + { + return selfPipeline.CreatePipelineFromInput(markdown); + } + return pipeline; + } + + /// /// Normalizes the specified markdown to a normalized markdown text. /// @@ -49,14 +68,15 @@ public static string Normalize(string markdown, NormalizeOptions? options = null /// A normalized markdown text. public static MarkdownDocument Normalize(string markdown, TextWriter writer, NormalizeOptions? options = null, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) { - pipeline ??= new MarkdownPipelineBuilder().Build(); - pipeline = CheckForSelfPipeline(pipeline, markdown); + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + + pipeline = GetPipeline(pipeline, markdown); + + var document = MarkdownParser.Parse(markdown, pipeline, context); - // We override the renderer with our own writer var renderer = new NormalizeRenderer(writer, options); pipeline.Setup(renderer); - var document = Parse(markdown, pipeline, context); renderer.Render(document); writer.Flush(); @@ -74,18 +94,12 @@ public static MarkdownDocument Normalize(string markdown, TextWriter writer, Nor public static string ToHtml(string markdown, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) { if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - pipeline ??= new MarkdownPipelineBuilder().Build(); - pipeline = CheckForSelfPipeline(pipeline, markdown); - var renderer = pipeline.GetCacheableHtmlRenderer(); + pipeline = GetPipeline(pipeline, markdown); - var document = Parse(markdown, pipeline, context); - renderer.Render(document); - renderer.Writer.Flush(); + var document = MarkdownParser.Parse(markdown, pipeline, context); - string html = renderer.Writer.ToString()!; - pipeline.ReleaseCacheableHtmlRenderer(renderer); - return html; + return ToHtml(document, pipeline); } /// @@ -95,19 +109,19 @@ public static string ToHtml(string markdown, MarkdownPipeline? pipeline = null, /// The pipeline used for the conversion. /// The result of the conversion /// if markdown document variable is null - public static string ToHtml(this MarkdownDocument document, MarkdownPipeline pipeline = null) + public static string ToHtml(this MarkdownDocument document, MarkdownPipeline? pipeline = null) { - if (document == null) ThrowHelper.ArgumentNullException(nameof(document)); - pipeline ??= new MarkdownPipelineBuilder().Build(); + if (document is null) ThrowHelper.ArgumentNullException(nameof(document)); + + pipeline ??= _defaultPipeline; - var renderer = pipeline.GetCacheableHtmlRenderer(); + using var rentedRenderer = pipeline.RentHtmlRenderer(); + HtmlRenderer renderer = rentedRenderer.Instance; renderer.Render(document); renderer.Writer.Flush(); - string html = renderer.Writer.ToString(); - pipeline.ReleaseCacheableHtmlRenderer(renderer); - return html; + return renderer.Writer.ToString() ?? string.Empty; } /// @@ -123,14 +137,14 @@ public static MarkdownDocument ToHtml(string markdown, TextWriter writer, Markdo { if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); if (writer is null) ThrowHelper.ArgumentNullException_writer(); - pipeline ??= new MarkdownPipelineBuilder().Build(); - pipeline = CheckForSelfPipeline(pipeline, markdown); - // We override the renderer with our own writer - var renderer = new HtmlRenderer(writer); - pipeline.Setup(renderer); + pipeline = GetPipeline(pipeline, markdown); + + var document = MarkdownParser.Parse(markdown, pipeline, context); + + using var rentedRenderer = pipeline.RentHtmlRenderer(writer); + HtmlRenderer renderer = rentedRenderer.Instance; - var document = Parse(markdown, pipeline, context); renderer.Render(document); writer.Flush(); @@ -149,10 +163,11 @@ public static object Convert(string markdown, IMarkdownRenderer renderer, Markdo { if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); if (renderer is null) ThrowHelper.ArgumentNullException(nameof(renderer)); - pipeline ??= new MarkdownPipelineBuilder().Build(); - pipeline = CheckForSelfPipeline(pipeline, markdown); - var document = Parse(markdown, pipeline, context); + pipeline = GetPipeline(pipeline, markdown); + + var document = MarkdownParser.Parse(markdown, pipeline, context); + pipeline.Setup(renderer); return renderer.Render(document); } @@ -168,9 +183,7 @@ public static MarkdownDocument Parse(string markdown, bool trackTrivia = false) { if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - MarkdownPipeline? pipeline = trackTrivia ? new MarkdownPipelineBuilder() - .EnableTrackTrivia() - .Build() : null; + MarkdownPipeline? pipeline = trackTrivia ? _defaultTrackTriviaPipeline : null; return Parse(markdown, pipeline); } @@ -186,20 +199,10 @@ public static MarkdownDocument Parse(string markdown, bool trackTrivia = false) public static MarkdownDocument Parse(string markdown, MarkdownPipeline? pipeline, MarkdownParserContext? context = null) { if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); - pipeline ??= new MarkdownPipelineBuilder().Build(); - pipeline = CheckForSelfPipeline(pipeline, markdown); - return MarkdownParser.Parse(markdown, pipeline, context); - } + pipeline = GetPipeline(pipeline, markdown); - private static MarkdownPipeline CheckForSelfPipeline(MarkdownPipeline pipeline, string markdown) - { - var selfPipeline = pipeline.Extensions.Find(); - if (selfPipeline != null) - { - return selfPipeline.CreatePipelineFromInput(markdown); - } - return pipeline; + return MarkdownParser.Parse(markdown, pipeline, context); } /// @@ -213,10 +216,12 @@ private static MarkdownPipeline CheckForSelfPipeline(MarkdownPipeline pipeline, /// if reader or writer variable are null public static MarkdownDocument ToPlainText(string markdown, TextWriter writer, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) { - if (markdown == null) ThrowHelper.ArgumentNullException_markdown(); - if (writer == null) ThrowHelper.ArgumentNullException_writer(); - pipeline ??= new MarkdownPipelineBuilder().Build(); - pipeline = CheckForSelfPipeline(pipeline, markdown); + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); + if (writer is null) ThrowHelper.ArgumentNullException_writer(); + + pipeline = GetPipeline(pipeline, markdown); + + var document = MarkdownParser.Parse(markdown, pipeline, context); // We override the renderer with our own writer var renderer = new HtmlRenderer(writer) @@ -227,7 +232,6 @@ public static MarkdownDocument ToPlainText(string markdown, TextWriter writer, M }; pipeline.Setup(renderer); - var document = Parse(markdown, pipeline, context); renderer.Render(document); writer.Flush(); @@ -244,7 +248,7 @@ public static MarkdownDocument ToPlainText(string markdown, TextWriter writer, M /// if markdown variable is null public static string ToPlainText(string markdown, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null) { - if (markdown == null) ThrowHelper.ArgumentNullException_markdown(); + if (markdown is null) ThrowHelper.ArgumentNullException_markdown(); var writer = new StringWriter(); ToPlainText(markdown, writer, pipeline, context); return writer.ToString(); diff --git a/src/Markdig/MarkdownPipeline.cs b/src/Markdig/MarkdownPipeline.cs index 726770bd9..6ab8c661d 100644 --- a/src/Markdig/MarkdownPipeline.cs +++ b/src/Markdig/MarkdownPipeline.cs @@ -4,6 +4,7 @@ using System; using System.IO; +using System.Text; using Markdig.Helpers; using Markdig.Parsers; using Markdig.Renderers; @@ -68,40 +69,76 @@ public void Setup(IMarkdownRenderer renderer) } - private HtmlRendererCache _rendererCache = null; + private HtmlRendererCache _rendererCache, _rendererCacheForCustomWriter; - internal HtmlRenderer GetCacheableHtmlRenderer() + internal RentedHtmlRenderer RentHtmlRenderer(TextWriter writer = null) { - if (_rendererCache is null) + HtmlRendererCache cache = writer is null + ? _rendererCache ??= new HtmlRendererCache(this, customWriter: false) + : _rendererCacheForCustomWriter ??= new HtmlRendererCache(this, customWriter: true); + + HtmlRenderer renderer = cache.Get(); + + if (writer is not null) { - _rendererCache = new HtmlRendererCache - { - OnNewInstanceCreated = Setup - }; + renderer.Writer = writer; } - return _rendererCache.Get(); - } - internal void ReleaseCacheableHtmlRenderer(HtmlRenderer renderer) - { - _rendererCache.Release(renderer); + + return new RentedHtmlRenderer(cache, renderer); } - private sealed class HtmlRendererCache : ObjectCache + internal sealed class HtmlRendererCache : ObjectCache { - public Action OnNewInstanceCreated; + private const int InitialCapacity = 1024; + + private static readonly StringWriter _dummyWriter = new(); + + private readonly MarkdownPipeline _pipeline; + private readonly bool _customWriter; + + public HtmlRendererCache(MarkdownPipeline pipeline, bool customWriter = false) + { + _pipeline = pipeline; + _customWriter = customWriter; + } protected override HtmlRenderer NewInstance() { - var writer = new StringWriter(); + var writer = _customWriter ? _dummyWriter : new StringWriter(new StringBuilder(InitialCapacity)); var renderer = new HtmlRenderer(writer); - OnNewInstanceCreated(renderer); + _pipeline.Setup(renderer); return renderer; } protected override void Reset(HtmlRenderer instance) { - instance.Reset(); + instance.ResetInternal(); + + if (_customWriter) + { + instance.Writer = _dummyWriter; + } + else + { + ((StringWriter)instance.Writer).GetStringBuilder().Length = 0; + } + } } + + internal readonly struct RentedHtmlRenderer : IDisposable + { + private readonly HtmlRendererCache _cache; + public readonly HtmlRenderer Instance; + + internal RentedHtmlRenderer(HtmlRendererCache cache, HtmlRenderer renderer) + { + _cache = cache; + Instance = renderer; + } + + public void Dispose() => _cache.Release(Instance); + } + } } \ No newline at end of file diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index af7d68515..07c24c1b9 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -155,28 +155,19 @@ private string FixupZero(string text) return text.Replace('\0', CharHelper.ReplacementChar); } - private sealed class ContainerItemCache : DefaultObjectCache - { - protected override void Reset(ContainerItem instance) - { - instance.Container = null; - instance.Index = 0; - } - } - private void ProcessInlines() { // "stackless" processor - var cache = new ContainerItemCache(); - var blocks = new Stack(); + int blockCount = 1; + var blocks = new ContainerItem[4]; - // TODO: Use an ObjectCache for ContainerItem - blocks.Push(new ContainerItem(document)); + blocks[0] = new ContainerItem(document); document.OnProcessInlinesBegin(inlineProcessor); - while (blocks.Count > 0) + + while (blockCount != 0) { process_new_block: - var item = blocks.Peek(); + ref ContainerItem item = ref blocks[blockCount - 1]; var container = item.Container; for (; item.Index < container.Count; item.Index++) @@ -212,35 +203,31 @@ private void ProcessInlines() // Else we have processed it item.Index++; } - var newItem = cache.Get(); - newItem.Container = (ContainerBlock)block; - block.OnProcessInlinesBegin(inlineProcessor); - newItem.Index = 0; - ThrowHelper.CheckDepthLimit(blocks.Count); - blocks.Push(newItem); + + if (blockCount == blocks.Length) + { + Array.Resize(ref blocks, blockCount * 2); + ThrowHelper.CheckDepthLimit(blocks.Length); + } + blocks[blockCount++] = new ContainerItem(newContainer); + newContainer.OnProcessInlinesBegin(inlineProcessor); goto process_new_block; } } - item = blocks.Pop(); - container = item.Container; container.OnProcessInlinesEnd(inlineProcessor); - - cache.Release(item); + blocks[--blockCount] = default; } } - private class ContainerItem + private struct ContainerItem { - public ContainerItem() - { - } - public ContainerItem(ContainerBlock container) { Container = container; + Index = 0; } - public ContainerBlock Container; + public readonly ContainerBlock Container; public int Index; } diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index f689cb2d2..62f716321 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -28,9 +28,7 @@ public abstract class TextRendererBase : RendererBase protected TextRendererBase(TextWriter writer) { if (writer == null) ThrowHelper.ArgumentNullException_writer(); - this.Writer = writer; - // By default we output a newline with '\n' only even on Windows platforms - Writer.NewLine = "\n"; + Writer = writer; } /// @@ -47,6 +45,8 @@ public TextWriter Writer ThrowHelper.ArgumentNullException(nameof(value)); } + // By default we output a newline with '\n' only even on Windows platforms + value.NewLine = "\n"; writer = value; } } @@ -128,6 +128,11 @@ protected internal void Reset() ThrowHelper.InvalidOperationException("Cannot reset this TextWriter instance"); } + ResetInternal(); + } + + internal void ResetInternal() + { childrenDepth = 0; previousWasLine = true; indents.Clear(); diff --git a/src/Markdig/Syntax/MarkdownObject.cs b/src/Markdig/Syntax/MarkdownObject.cs index 08fc9c24a..fd8d90db6 100644 --- a/src/Markdig/Syntax/MarkdownObject.cs +++ b/src/Markdig/Syntax/MarkdownObject.cs @@ -22,8 +22,7 @@ protected MarkdownObject() /// as we expect less than 5~10 entries, usually typically 1 (HtmlAttributes) /// so it will gives faster access than a Dictionary, and lower memory occupation /// - private DataEntry[] attachedDatas; - private int count; + private DataEntries _attachedDatas; /// /// Gets or sets the text column this instance was declared (zero-based). @@ -55,33 +54,7 @@ public string ToPositionText() /// The key. /// The value. /// if key is null - public void SetData(object key, object value) - { - if (key == null) ThrowHelper.ArgumentNullException_key(); - if (attachedDatas == null) - { - attachedDatas = new DataEntry[1]; - } - else - { - for (int i = 0; i < count; i++) - { - if (attachedDatas[i].Key == key) - { - attachedDatas[i].Value = value; - return; - } - } - if (count == attachedDatas.Length) - { - var temp = new DataEntry[attachedDatas.Length + 1]; - Array.Copy(attachedDatas, 0, temp, 0, count); - attachedDatas = temp; - } - } - attachedDatas[count] = new DataEntry(key, value); - count++; - } + public void SetData(object key, object value) => (_attachedDatas ??= new DataEntries()).SetData(key, value); /// /// Determines whether this instance contains the specified key data. @@ -89,23 +62,7 @@ public void SetData(object key, object value) /// The key. /// true if a data with the key is stored /// if key is null - public bool ContainsData(object key) - { - if (key == null) ThrowHelper.ArgumentNullException_key(); - if (attachedDatas == null) - { - return false; - } - - for (int i = 0; i < count; i++) - { - if (attachedDatas[i].Key == key) - { - return true; - } - } - return false; - } + public bool ContainsData(object key) => _attachedDatas?.ContainsData(key) ?? false; /// /// Gets the associated data for the specified key. @@ -113,22 +70,7 @@ public bool ContainsData(object key) /// The key. /// The associated data or null if none /// if key is null - public object GetData(object key) - { - if (key == null) ThrowHelper.ArgumentNullException_key(); - if (attachedDatas == null) - { - return null; - } - for (int i = 0; i < count; i++) - { - if (attachedDatas[i].Key == key) - { - return attachedDatas[i].Value; - } - } - return null; - } + public object GetData(object key) => _attachedDatas?.GetData(key); /// /// Removes the associated data for the specified key. @@ -136,44 +78,117 @@ public object GetData(object key) /// The key. /// true if the data was removed; false otherwise /// - public bool RemoveData(object key) + public bool RemoveData(object key) => _attachedDatas?.RemoveData(key) ?? false; + + private class DataEntries { - if (key == null) ThrowHelper.ArgumentNullException_key(); - if (attachedDatas == null) + private struct DataEntry + { + public readonly object Key; + public object Value; + + public DataEntry(object key, object value) + { + Key = key; + Value = value; + } + } + + private DataEntry[] _entries; + private int _count; + + public DataEntries() { - return true; + _entries = new DataEntry[2]; } - for (int i = 0; i < count; i++) + public void SetData(object key, object value) { - if (attachedDatas[i].Key == key) + if (key == null) ThrowHelper.ArgumentNullException_key(); + + DataEntry[] entries = _entries; + int count = _count; + + for (int i = 0; i < entries.Length && i < count; i++) { - if (i < count - 1) + ref DataEntry entry = ref entries[i]; + if (entry.Key == key) { - Array.Copy(attachedDatas, i + 1, attachedDatas, i, count - i - 1); + entry.Value = value; + return; } - count--; - attachedDatas[count] = new DataEntry(); - return true; } + + if (count == entries.Length) + { + Array.Resize(ref _entries, count + 2); + } + + _entries[count] = new DataEntry(key, value); + _count++; } - return false; - } - /// - /// Store a Key/Value pair. - /// - private struct DataEntry - { - public DataEntry(object key, object value) + public object GetData(object key) { - Key = key; - Value = value; + if (key == null) ThrowHelper.ArgumentNullException_key(); + + DataEntry[] entries = _entries; + int count = _count; + + for (int i = 0; i < entries.Length && i < count; i++) + { + ref DataEntry entry = ref entries[i]; + if (entry.Key == key) + { + return entry.Value; + } + } + + return null; } - public readonly object Key; + public bool ContainsData(object key) + { + if (key == null) ThrowHelper.ArgumentNullException_key(); - public object Value; + DataEntry[] entries = _entries; + int count = _count; + + for (int i = 0; i < entries.Length && i < count; i++) + { + if (entries[i].Key == key) + { + return true; + } + } + + return false; + } + + public bool RemoveData(object key) + { + if (key == null) ThrowHelper.ArgumentNullException_key(); + + DataEntry[] entries = _entries; + int count = _count; + + for (int i = 0; i < entries.Length && i < count; i++) + { + if (entries[i].Key == key) + { + if (i < count - 1) + { + Array.Copy(entries, i + 1, entries, i, count - i - 1); + } + count--; + entries[count] = default; + _count = count; + return true; + } + } + + return false; + } } } } \ No newline at end of file