Древовидная структура данных в C #


248

Я искал древовидную или графическую структуру данных в C #, но, по-моему, ее нет. Обширный анализ структур данных с использованием C # 2.0 объясняет немного, почему. Есть ли удобная библиотека, которая обычно используется для обеспечения этой функциональности? Возможно, через шаблон стратегии для решения вопросов, представленных в статье.

Я чувствую себя немного глупо, реализуя свое собственное дерево, так же, как я реализую свой собственный ArrayList.

Я просто хочу общее дерево, которое может быть несбалансированным. Подумайте о дереве каталогов. C5 выглядит изящно, но их древовидные структуры кажутся реализованными в виде сбалансированных красно-черных деревьев, которые лучше подходят для поиска, чем для представления иерархии узлов.


2
Немного более экстремальных деревьев: stackoverflow.com/questions/196294/… ;-)
Туомас Хиетанен,

Есть ли какая-то причина, по которой нельзя включить TreeView в проект и использовать его? Нет причин показывать это пользователю. Конечно, есть несколько форм проектов, когда это не вариант. Всегда можно создать новые классы, которые наследуются от примера TreeNode, если нужна особая сложность?
Просто Г.

9
Я бы посчитал плохой идеей импортировать всю библиотеку пользовательского интерфейса для очень простого дерева.
стимулирует

1
Не могли бы вы мотивировать? Это не похоже на фактическое требование места на жестком диске, проблема больше? Неуклюжий? Как я упоминал ранее, я могу понять, что это не решение для специализированного программного обеспечения или чего-то другого без существующего пользовательского интерфейса. Я ленивый программист, если я могу получить структуру бесплатно, все хорошо. И в существующей библиотеке есть много бесплатного, можно найти много кода от людей, которые использовали его для многих вещей.
Просто Г.

1
Вот простой тип дерева: public class Tree<T> : List<Tree<T>> { public T Value; }.
Загадка

Ответы:


155

Мой лучший совет: стандартная древовидная структура данных не существует, потому что есть так много способов ее реализовать, что невозможно охватить все базы одним решением. Чем конкретнее решение, тем менее вероятно, что оно применимо к любой конкретной проблеме. Меня даже раздражает LinkedList - что если я хочу круглый список ссылок?

Базовая структура, которую вам нужно реализовать, будет набором узлов, и вот несколько вариантов, с которых можно начать. Давайте предположим, что класс Node является базовым классом всего решения.

Если вам нужно перемещаться только по дереву, то классу Node необходим список дочерних элементов.

Если вам нужно перемещаться вверх по дереву, то классу Node нужна ссылка на его родительский узел.

Создайте метод AddChild, который позаботится обо всех мелочах этих двух точек и любой другой бизнес-логике, которая должна быть реализована (дочерние ограничения, сортировка дочерних элементов и т. Д.)


5
лично я не возражаю против того, чтобы в библиотеку было добавлено какое-то самобалансирующееся двоичное дерево, так как это дополнительная работа, а не просто использование смежного списка.
JK.

8
@jk Я считаю, что SortedDictionary и SortedSet построены на красных / черных деревьях, поэтому их использование должно работать.
2010 г.

Взгляните на составной шаблон ;-) Именно то, что вы ищете
Николас Ворон

119
delegate void TreeVisitor<T>(T nodeData);

class NTree<T>
{
    private T data;
    private LinkedList<NTree<T>> children;

    public NTree(T data)
    {
         this.data = data;
        children = new LinkedList<NTree<T>>();
    }

    public void AddChild(T data)
    {
        children.AddFirst(new NTree<T>(data));
    }

    public NTree<T> GetChild(int i)
    {
        foreach (NTree<T> n in children)
            if (--i == 0)
                return n;
        return null;
    }

    public void Traverse(NTree<T> node, TreeVisitor<T> visitor)
    {
        visitor(node.data);
        foreach (NTree<T> kid in node.children)
            Traverse(kid, visitor);
    }
}

Простая рекурсивная реализация ... <40 строк кода ... Вам просто нужно сохранить ссылку на корень дерева вне класса или обернуть его в другом классе, возможно, переименовать в TreeNode ??


22
В этом случае, в C # в любом случае, вы можете не писать свой собственный делегат и использовать предварительно сделанный Action<T>делегат: public void traverse(NTree<T> node, Action<T> visitor). Действие <> 's подпись: void Action<T>( T obj ). Также есть версии от 0 до 4 разных параметров. Также есть аналогичный делегат для вызываемых функций Func<>.
Бенни Джобиган

2
как бы я назвал этого делегата?
Странно

3
было бы неплохо изменить метод траверсы на статический или, возможно, обернуть его, чтобы скрыть рекурсивную природу, но его просто пересечь: создайте метод с подписью делегата, т. е. для дерева целых: void my_visitor_impl (int datum) - сделайте его статическим, если вам нужно, создайте экземпляр delgate: TreeVisitor <int> my_visitor = my_visitor_impl; и затем вызовите корневой узел или класс NTree, если вы сделаете его статичным: NTree <int> .traverse (my_tree, my_visitor)
Аарон Гейдж

10
Заставление addChild () вернуть NTree, которое он добавил, сделает его более приятным для добавления данных в дерево. (Если только мне не хватает хитрого способа построить дерево с этим, не полагаясь на детали реализации, которые недавно добавленный дочерний элемент == getChild (1)?)
Рори

1
Я думаю, что это утверждение --i == 0будет функционировать только в одном случае? Это правда. Это
Васим Ахмад Наим

57

Вот мой, который очень похож на Аарона Гейджа , на мой взгляд, немного более обычный. В моих целях у меня не было проблем с производительностью List<T>. Было бы достаточно легко переключиться на LinkedList, если это необходимо.


namespace Overby.Collections
{
    public class TreeNode<T>
    {
        private readonly T _value;
        private readonly List<TreeNode<T>> _children = new List<TreeNode<T>>();

        public TreeNode(T value)
        {
            _value = value;
        }

        public TreeNode<T> this[int i]
        {
            get { return _children[i]; }
        }

        public TreeNode<T> Parent { get; private set; }

        public T Value { get { return _value; } }

        public ReadOnlyCollection<TreeNode<T>> Children
        {
            get { return _children.AsReadOnly(); }
        }

        public TreeNode<T> AddChild(T value)
        {
            var node = new TreeNode<T>(value) {Parent = this};
            _children.Add(node);
            return node;
        }

        public TreeNode<T>[] AddChildren(params T[] values)
        {
            return values.Select(AddChild).ToArray();
        }

        public bool RemoveChild(TreeNode<T> node)
        {
            return _children.Remove(node);
        }

        public void Traverse(Action<T> action)
        {
            action(Value);
            foreach (var child in _children)
                child.Traverse(action);
        }

        public IEnumerable<T> Flatten()
        {
            return new[] {Value}.Concat(_children.SelectMany(x => x.Flatten()));
        }
    }
}

почему ваше свойство Value отображается, когда вы устанавливаете его в конструкторе? что оставляет его открытым для манипуляций ПОСЛЕ того, как вы уже установили его с помощью конструктора, верно? Должен быть приватный набор?
PositiveGuy

Конечно, почему бы не сделать его неизменным? Ред.
Ронни Оверби

Спасибо! Мне очень нравилось не писать свои собственные. (До сих пор не могу поверить, что это не то, что существует изначально. Я всегда думал, что .net, или, по крайней мере, .net 4.0, имел все .)
neminem

3
Мне понравилось это решение. Я также обнаружил, что мне нужно вставить, я добавил следующий метод, чтобы сделать это. public TreeNode<T> InsertChild(TreeNode<T> parent, T value) { var node = new TreeNode<T>(value) { Parent = parent }; parent._children.Add(node); return node; } var five = myTree.AddChild(5); myTree.InsertChild(five, 55);
JabberwockyDecompiler

48

Еще одна древовидная структура:

public class TreeNode<T> : IEnumerable<TreeNode<T>>
{

    public T Data { get; set; }
    public TreeNode<T> Parent { get; set; }
    public ICollection<TreeNode<T>> Children { get; set; }

    public TreeNode(T data)
    {
        this.Data = data;
        this.Children = new LinkedList<TreeNode<T>>();
    }

    public TreeNode<T> AddChild(T child)
    {
        TreeNode<T> childNode = new TreeNode<T>(child) { Parent = this };
        this.Children.Add(childNode);
        return childNode;
    }

    ... // for iterator details see below link
}

Пример использования:

TreeNode<string> root = new TreeNode<string>("root");
{
    TreeNode<string> node0 = root.AddChild("node0");
    TreeNode<string> node1 = root.AddChild("node1");
    TreeNode<string> node2 = root.AddChild("node2");
    {
        TreeNode<string> node20 = node2.AddChild(null);
        TreeNode<string> node21 = node2.AddChild("node21");
        {
            TreeNode<string> node210 = node21.AddChild("node210");
            TreeNode<string> node211 = node21.AddChild("node211");
        }
    }
    TreeNode<string> node3 = root.AddChild("node3");
    {
        TreeNode<string> node30 = node3.AddChild("node30");
    }
}

БОНУС
Видеть полноценное дерево с:

  • итератор
  • поиск
  • Java / C #

https://github.com/gt4dev/yet-another-tree-structure


Как мне использовать поиск в вашем примере кода? Откуда nodeберутся? Значит ли это, что мне нужно перебирать дерево, чтобы использовать код поиска?
BadmintonCat

@GrzegorzDev Может быть -1, потому что он не реализует все IEnumerable<>члены, поэтому он не компилируется.
Уве Кейм

1
@UweKeim Good Job, в следующий раз попробуйте использовать код с реальными значениями.
szab.kel

Единственная проблема, которую я вижу, состоит в том, что она не будет правильно сериализована с базовым JsonConvert, так как она реализует IEnumerable <>
Rakiah

22

В целом отличная библиотека C5 Generic Collection Library имеет несколько различных древовидных структур данных, включая наборы, пакеты и словари. Исходный код доступен, если вы хотите изучить детали их реализации. (Я использовал коллекции C5 в производственном коде с хорошими результатами, хотя я специально не использовал ни одну из древовидных структур.)


7
Не знаю, возможно ли что-то изменилось, но сейчас книга свободно доступна для скачивания в формате PDF с сайта C5.
Оскар

4
Нехватка документации больше не вызывает беспокойства, поскольку библиотека дополняется 272 страницами в формате PDF ... Не могу прокомментировать качество кода, но, судя по качеству документации, я действительно с нетерпением жду, чтобы окунуться в этот вечер!
Флориан Дойон

2
Насколько я понимаю, эта библиотека C5 вообще не имеет деревьев, а имеет только некоторые структуры данных, полученные из дерева.
Рим

10

Смотрите http://quickgraph.codeplex.com/

QuickGraph предоставляет общие структуры и алгоритмы ориентированных / неориентированных графов для .Net 2.0 и выше. QuickGraph поставляется с такими алгоритмами, как поиск по глубине, поиск по дыханию, поиск A *, кратчайший путь, k-кратчайший путь, максимальный поток, минимальное связующее дерево, наименьшие общие предки и т. Д. QuickGraph поддерживает MSAGL, GLEE и Graphviz для рендеринг графиков, сериализация в GraphML и т. д.


8

Если вы хотите написать свой собственный, вы можете начать с этого документа, состоящего из шести частей, подробно описывающего эффективное использование структур данных C # 2.0 и как анализировать реализацию ваших структур данных в C #. В каждой статье есть примеры и установщик с образцами, за которыми вы можете следить.

«Обширный анализ структур данных с использованием C # 2.0» Скотта Митчелла


7

У меня есть небольшое расширение для решений.

Используя рекурсивное обобщенное объявление и производный подкласс, вы можете лучше сконцентрироваться на своей реальной цели.

Обратите внимание, что это отличается от неуниверсальной реализации, вам не нужно приводить 'node' в 'NodeWorker'.

Вот мой пример:

public class GenericTree<T> where T : GenericTree<T> // recursive constraint  
{
  // no specific data declaration  

  protected List<T> children;

  public GenericTree()
  {
    this.children = new List<T>();
  }

  public virtual void AddChild(T newChild)
  {
    this.children.Add(newChild);
  }

  public void Traverse(Action<int, T> visitor)
  {
    this.traverse(0, visitor);
  }

  protected virtual void traverse(int depth, Action<int, T> visitor)
  {
    visitor(depth, (T)this);
    foreach (T child in this.children)
      child.traverse(depth + 1, visitor);
  }
}

public class GenericTreeNext : GenericTree<GenericTreeNext> // concrete derivation
{
  public string Name {get; set;} // user-data example

  public GenericTreeNext(string name)
  {
    this.Name = name;
  }
}

static void Main(string[] args)  
{  
  GenericTreeNext tree = new GenericTreeNext("Main-Harry");  
  tree.AddChild(new GenericTreeNext("Main-Sub-Willy"));  
  GenericTreeNext inter = new GenericTreeNext("Main-Inter-Willy");  
  inter.AddChild(new GenericTreeNext("Inter-Sub-Tom"));  
  inter.AddChild(new GenericTreeNext("Inter-Sub-Magda"));  
  tree.AddChild(inter);  
  tree.AddChild(new GenericTreeNext("Main-Sub-Chantal"));  
  tree.Traverse(NodeWorker);  
}  

static void NodeWorker(int depth, GenericTreeNext node)  
{                                // a little one-line string-concatenation (n-times)
  Console.WriteLine("{0}{1}: {2}", String.Join("   ", new string[depth + 1]), depth, node.Name);  
}  

что такое глубина и откуда и как ее взять?
PositiveGuy

@ WeDoTDD.com, глядя на его класс, вы видите, что Traverse объявляет его как 0 для начала в корневом узле, а затем использует метод traverse, добавляющий к этому int каждую итерацию.
Эдвард

Как бы вы искали целое дерево для определенного узла?
Mattpm

6

Вот мой собственный:

class Program
{
    static void Main(string[] args)
    {
        var tree = new Tree<string>()
            .Begin("Fastfood")
                .Begin("Pizza")
                    .Add("Margherita")
                    .Add("Marinara")
                .End()
                .Begin("Burger")
                    .Add("Cheese burger")
                    .Add("Chili burger")
                    .Add("Rice burger")
                .End()
            .End();

        tree.Nodes.ForEach(p => PrintNode(p, 0));
        Console.ReadKey();
    }

    static void PrintNode<T>(TreeNode<T> node, int level)
    {
        Console.WriteLine("{0}{1}", new string(' ', level * 3), node.Value);
        level++;
        node.Children.ForEach(p => PrintNode(p, level));
    }
}

public class Tree<T>
{
    private Stack<TreeNode<T>> m_Stack = new Stack<TreeNode<T>>();

    public List<TreeNode<T>> Nodes { get; } = new List<TreeNode<T>>();

    public Tree<T> Begin(T val)
    {
        if (m_Stack.Count == 0)
        {
            var node = new TreeNode<T>(val, null);
            Nodes.Add(node);
            m_Stack.Push(node);
        }
        else
        {
            var node = m_Stack.Peek().Add(val);
            m_Stack.Push(node);
        }

        return this;
    }

    public Tree<T> Add(T val)
    {
        m_Stack.Peek().Add(val);
        return this;
    }

    public Tree<T> End()
    {
        m_Stack.Pop();
        return this;
    }
}

public class TreeNode<T>
{
    public T Value { get; }
    public TreeNode<T> Parent { get; }
    public List<TreeNode<T>> Children { get; }

    public TreeNode(T val, TreeNode<T> parent)
    {
        Value = val;
        Parent = parent;
        Children = new List<TreeNode<T>>();
    }

    public TreeNode<T> Add(T val)
    {
        var node = new TreeNode<T>(val, this);
        Children.Add(node);
        return node;
    }
}

Вывод:

Fastfood
   Pizza
      Margherita
      Marinara
   Burger
      Cheese burger
      Chili burger
      Rice burger

4

Попробуйте этот простой пример.

public class TreeNode<TValue>
{
    #region Properties
    public TValue Value { get; set; }
    public List<TreeNode<TValue>> Children { get; private set; }
    public bool HasChild { get { return Children.Any(); } }
    #endregion
    #region Constructor
    public TreeNode()
    {
        this.Children = new List<TreeNode<TValue>>();
    }
    public TreeNode(TValue value)
        : this()
    {
        this.Value = value;
    }
    #endregion
    #region Methods
    public void AddChild(TreeNode<TValue> treeNode)
    {
        Children.Add(treeNode);
    }
    public void AddChild(TValue value)
    {
        var treeNode = new TreeNode<TValue>(value);
        AddChild(treeNode);
    }
    #endregion
}

2

Я создаю класс Node, который может быть полезен для других людей. Класс имеет такие свойства, как:

  • Дети
  • Предки
  • Потомки
  • Братья и сестры
  • Уровень узла
  • родитель
  • корень
  • И т.п.

Существует также возможность преобразовать плоский список элементов с Id и ParentId в дерево. Узлы содержат ссылку как на дочерние, так и на родительские элементы, что делает итерацию узлов достаточно быстрой.


2

Поскольку это не упомянуто, я хотел бы, чтобы вы обратили внимание на уже выпущенную кодовую базу .net: в частности, код для, SortedSetкоторый реализует Red-Black-Tree:

https://github.com/Microsoft/referencesource/blob/master/System/compmod/system/collections/generic/sortedset.cs

Это, однако, сбалансированная древовидная структура. Поэтому мой ответ - скорее ссылка на то, что я считаю единственной нативной древовидной структурой в базовой библиотеке .net.


2

Я выполнил код, которым поделился @Berezh.

  public class TreeNode<T> : IEnumerable<TreeNode<T>>
    {

        public T Data { get; set; }
        public TreeNode<T> Parent { get; set; }
        public ICollection<TreeNode<T>> Children { get; set; }

        public TreeNode(T data)
        {
            this.Data = data;
            this.Children = new LinkedList<TreeNode<T>>();
        }

        public TreeNode<T> AddChild(T child)
        {
            TreeNode<T> childNode = new TreeNode<T>(child) { Parent = this };
            this.Children.Add(childNode);
            return childNode;
        }

        public IEnumerator<TreeNode<T>> GetEnumerator()
        {
            throw new NotImplementedException();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (IEnumerator)GetEnumerator();
        }
    }
    public class TreeNodeEnum<T> : IEnumerator<TreeNode<T>>
    {

        int position = -1;
        public List<TreeNode<T>> Nodes { get; set; }

        public TreeNode<T> Current
        {
            get
            {
                try
                {
                    return Nodes[position];
                }
                catch (IndexOutOfRangeException)
                {
                    throw new InvalidOperationException();
                }
            }
        }


        object IEnumerator.Current
        {
            get
            {
                return Current;
            }
        }


        public TreeNodeEnum(List<TreeNode<T>> nodes)
        {
            Nodes = nodes;
        }

        public void Dispose()
        {
        }

        public bool MoveNext()
        {
            position++;
            return (position < Nodes.Count);
        }

        public void Reset()
        {
            position = -1;
        }
    }

Хороший дизайн. Однако я не уверен, является ли узел «последовательностью» его дочернего узла. Я бы рассмотрел следующее: у узла «есть» ноль или более дочерних узлов, поэтому узел не является производным от последовательности дочерних узлов, но это совокупность (состав?) Его дочерних узлов
Харальд Копполс,

2

Вот дерево

public class Tree<T> : List<Tree<T>>
{
    public  T Data { get; private set; }

    public Tree(T data)
    {
        this.Data = data;
    }

    public Tree<T> Add(T data)
    {
        var node = new Tree<T>(data);
        this.Add(node);
        return node;
    }
}

Вы даже можете использовать инициализаторы:

    var tree = new Tree<string>("root")
    {
        new Tree<string>("sample")
        {
            "console1"
        }
    };

1

Большинство деревьев сформированы данными, которые вы обрабатываете.

Скажем, у вас есть personкласс, который включает в себя чьи-то сведения parents, вы бы предпочли, чтобы древовидная структура была частью вашего «класса домена», или вы используете отдельный класс дерева, который содержал ссылки на ваши личные объекты? Подумайте о простой операции , как получение все grandchildrenиз person, это должно быть код в person классе, или если пользователь personкласса должен знать о отдельном классе дерева?

Другой пример - дерево разбора в компиляторе…

Оба этих примера показывают, что концепция дерева является частью домена данных, и использование отдельного дерева общего назначения по крайней мере удваивает количество создаваемых объектов, а также усложняет программирование API.

Нам нужен способ многократного использования стандартных операций с деревом, без необходимости повторной реализации их для всех деревьев, и в то же время без необходимости использовать стандартный класс дерева. Boost пытался решить проблему такого типа для C ++, но я пока не вижу каких-либо эффектов для .NET.


@Puchacz, извините, у меня уже 15 лет нет данных по C ++, взгляните на Boost и Templates, после нескольких слабых занятий вы можете их понять. Власть имеет высокую стоимость обучения!
Ян Рингроз

1

Я добавил полное решение и пример, используя класс NTree выше, а также добавил метод "AddChild" ...

    public class NTree<T>
    {
        public T data;
        public LinkedList<NTree<T>> children;

        public NTree(T data)
        {
            this.data = data;
            children = new LinkedList<NTree<T>>();
        }

        public void AddChild(T data)
        {
            var node = new NTree<T>(data) { Parent = this };
            children.AddFirst(node);
        }

        public NTree<T> Parent { get; private set; }

        public NTree<T> GetChild(int i)
        {
            foreach (NTree<T> n in children)
                if (--i == 0)
                    return n;
            return null;
        }

        public void Traverse(NTree<T> node, TreeVisitor<T> visitor, string t, ref NTree<T> r)
        {
            visitor(node.data, node, t, ref r);
            foreach (NTree<T> kid in node.children)
                Traverse(kid, visitor, t, ref r);
        }
    }
    public static void DelegateMethod(KeyValuePair<string, string> data, NTree<KeyValuePair<string, string>> node, string t, ref NTree<KeyValuePair<string, string>> r)
    {
        string a = string.Empty;
        if (node.data.Key == t)
        {
            r = node;
            return;
        }
    }

с помощью

 NTree<KeyValuePair<string, string>> ret = null;
 tree.Traverse(tree, DelegateMethod, node["categoryId"].InnerText, ref ret);

Должен ли траверс быть статическим методом? Это кажется очень неуклюжим как метод экземпляра, передающий себя в себя
Sinaesthetic

0

Вот моя реализация BST

class BST
{
    public class Node
    {
        public Node Left { get; set; }
        public object Data { get; set; }
        public Node Right { get; set; }

        public Node()
        {
            Data = null;
        }

        public Node(int Data)
        {
            this.Data = (object)Data;
        }

        public void Insert(int Data)
        {
            if (this.Data == null)
            {
                this.Data = (object)Data;
                return;
            }
            if (Data > (int)this.Data)
            {
                if (this.Right == null)
                {
                    this.Right = new Node(Data);
                }
                else
                {
                    this.Right.Insert(Data);
                }
            }
            if (Data <= (int)this.Data)
            {
                if (this.Left == null)
                {
                    this.Left = new Node(Data);
                }
                else
                {
                    this.Left.Insert(Data);
                }
            }
        }

        public void TraverseInOrder()
        {
            if(this.Left != null)
                this.Left.TraverseInOrder();
            Console.Write("{0} ", this.Data);
            if (this.Right != null)
                this.Right.TraverseInOrder();
        }
    }

    public Node Root { get; set; }
    public BST()
    {
        Root = new Node();
    }
}

0

Если вы собираетесь отображать это дерево в графическом интерфейсе, вы можете использовать TreeView и TreeNode . (Полагаю, технически вы можете создать TreeNode, не помещая его в графический интерфейс, но он имеет больше накладных расходов, чем простая реализация TreeNode.)


-4

Если вам нужна реализация структуры данных с корневым деревом, которая использует меньше памяти, вы можете написать свой класс Node следующим образом (реализация C ++):

class Node {
       Node* parent;
       int item; // depending on your needs

       Node* firstChild; //pointer to left most child of node
       Node* nextSibling; //pointer to the sibling to the right
}

12
Публикация кода на языке C ++ специально для C # - не лучшая идея, Джейк. Особенно тот, который включает в себя указатели. Вы знаете, что в C # охотятся за указателями, не так ли? : p
ThunderGr

2
@ ThunderGr это не честно. Ответ на C # был бы лучше, но эти указатели на C ++ могут быть поняты C-спикерами как ссылки (они менее безопасны, хорошо). После того, как Дэвид Бойк, Аарон Гейдж, Ронни Оверби, Гжегож Дев, Береж и Эрик Нагель все предложили в основном одну и ту же структуру данных с небольшими различиями только в выражении, Джейк предложил разделить связанный список, получив более простые структуры с одним типом узла и навигация родного брата. Не выражайте свою неприязнь к C ++, отказавшись от конструктивного ответа.
Миг

3
@migle Я не понизил ответ (также не проголосовал). И я не люблю C ++. Я видел, что ответ был отвергнут, и никто ничего не предложил Джейку о том, почему и как он улучшит свой ответ. Это не о том, чтобы быть лучше. Вопрос помечен только для C #. Публикация ответов на языке, отличном от тега, не рекомендуется, и некоторые люди будут понижать голос.
ThunderGr
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.