Estructura de datos de árbol en C #


248

Estaba buscando una estructura de datos de árbol o gráfico en C #, pero supongo que no se proporciona ninguno. Un extenso examen de las estructuras de datos con C # 2.0 explica un poco sobre por qué. ¿Existe una biblioteca conveniente que se usa comúnmente para proporcionar esta funcionalidad? Quizás a través de un patrón de estrategia para resolver los problemas presentados en el artículo.

Me siento un poco tonto implementando mi propio árbol, tal como lo haría con mi propia ArrayList.

Solo quiero un árbol genérico que pueda estar desequilibrado. Piensa en un árbol de directorios. C5 parece ingenioso, pero sus estructuras de árbol parecen implementarse como árboles equilibrados rojo-negros más adecuados para la búsqueda que para representar una jerarquía de nodos.


2
Árboles un poco más extremos: stackoverflow.com/questions/196294/… ;-)
Tuomas Hietanen

¿Hay alguna razón por la que uno no puede incluir un TreeView en el proyecto y usarlo? No hay ninguna razón para mostrarlo realmente a un usuario. Por supuesto, hay varias formas de proyectos cuando esta no es una opción. Siempre se pueden crear nuevas clases que hereden del ejemplo TreeNode si se necesita una complejidad especial.
Simplemente G.

99
Consideraría que es una mala idea importar una biblioteca de interfaz de usuario completa para un árbol muy simple.
estimula el

1
¿Podrías motivar? ¿Ya no es un requisito real de espacio en el disco duro? ¿Torpe? Como mencioné antes, puedo entender que esta no es una solución para un software especializado o algo sin una interfaz de usuario existente. Soy un programador perezoso, si puedo obtener una estructura gratis, todo está bien. Y una biblioteca existente tiene mucho de forma gratuita, uno puede encontrar mucho código de personas que lo usaron para muchas cosas.
Simplemente G.

No estoy discutiendo, solo quiero saber tu razonamiento.
Simplemente G.

Respuestas:


155

Mi mejor consejo sería que no exista una estructura de datos de árbol estándar porque hay tantas maneras de implementarla que sería imposible cubrir todas las bases con una sola solución. Cuanto más específica sea una solución, es menos probable que sea aplicable a cualquier problema dado. Incluso me molesta con LinkedList, ¿qué pasa si quiero una lista circular vinculada?

La estructura básica que necesitará implementar será una colección de nodos, y aquí hay algunas opciones para comenzar. Supongamos que la clase Node es la clase base de toda la solución.

Si solo necesita navegar por el árbol, una clase Node necesita una Lista de hijos.

Si necesita navegar por el árbol, la clase Node necesita un enlace a su nodo padre.

Cree un método AddChild que se ocupe de todos los detalles de estos dos puntos y de cualquier otra lógica de negocios que deba implementarse (límites secundarios, clasificación de los elementos secundarios, etc.)


55
personalmente, no me importaría agregar algún tipo de árbol binario autoequilibrado a la biblioteca, ya que este es un trabajo adicional que simplemente usar una lista de adyacencias.
jk.

8
@jk Creo que SortedDictionary y SortedSet están construidos sobre árboles rojos / negros, por lo que usarlos debería funcionar.
jonp

Eche un vistazo al patrón compuesto ;-) Exactamente lo que está buscando
Nicolas Voron

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);
    }
}

Implementación recursiva simple ... <40 líneas de código ... Solo necesita mantener una referencia a la raíz del árbol fuera de la clase, o envolverla en otra clase, ¿tal vez cambiar el nombre a TreeNode?


22
En este caso, en C # todos modos, se podría evitar la escritura de su propio delegado y el uso de la pre-hechos Action<T>delegado: public void traverse(NTree<T> node, Action<T> visitor). Acción <> 's firma es: void Action<T>( T obj ). También hay versiones de 0 a 4 parámetros diferentes. También hay un delegado análogo para funciones llamado Func<>.
Benny Jobigan

2
¿Cómo llamaría a este delegado?
Freakishly

3
cambiar el método transversal para que sea estático o posiblemente envolverlo para ocultar la naturaleza recursiva sería una buena idea, pero es fácil de recorrer: cree un método con la firma del delegado, es decir, para un árbol de entradas: void my_visitor_impl (int datum) - Hágalo estático si lo necesita, cree una instancia de un delegado: TreeVisitor <int> my_visitor = my_visitor_impl; y luego invoque en el nodo raíz o la clase NTree si lo hace estático: NTree <int> .traverse (my_tree, my_visitor)
Aaron Gage

10
Hacer que addChild () devuelva el NTree que agregó lo haría más agradable para agregar datos a un árbol. (A menos que me falte una manera astuta de construir un árbol con esto, sin depender de los detalles de implementación que un niño recién agregado == getChild (1)?)
Rory

1
¿Creo que esta afirmación --i == 0funcionará solo en un solo caso? Es esto cierto. Esto me hizo confundir
Waseem Ahmad Naeem

57

Aquí está el mío, que es muy similar al de Aaron Gage , un poco más convencional, en mi opinión. Para mis propósitos, no me he encontrado con ningún problema de rendimiento con List<T>. Sería bastante fácil cambiar a LinkedList si fuera necesario.


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()));
        }
    }
}

¿Por qué está expuesta su propiedad Value cuando la configura en el constructor? eso lo deja abierto para la manipulación DESPUÉS de que ya lo haya configurado a través del constructor ¿verdad? ¿Debe ser un conjunto privado?
PositiveGuy

Claro, ¿por qué no hacerlo inmutable? Editado
Ronnie Overby

¡Gracias! Me gustó mucho no tener que escribir el mío. (Todavía no puedo creer que no exista de forma nativa. Siempre pensé que .net, o al menos .net 4.0, lo tenía todo .)
neminem

3
Me gustó esta solución. También descubrí que necesitaba insertar, agregué el siguiente método para hacerlo. 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

Otra estructura de árbol más:

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
}

Uso de la muestra:

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");
    }
}

BONIFICACIÓN
Ver árbol de pleno derecho con:

  • iterador
  • buscando
  • Java / C #

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


¿Cómo uso la búsqueda en su ejemplo de código? De donde nodeviene ¿Significa que tengo que iterar sobre el árbol para usar el código de búsqueda?
BadmintonCat

@GrzegorzDev Quizás -1 porque no implementa todos los IEnumerable<>miembros, por lo que no se compila.
Uwe Keim

1
@UweKeim Good Job, la próxima vez intente usar el código con los usos reales.
szab.kel

El único problema que veo es que no se serializará correctamente con JsonConvert básico, ya que implementa IEnumerable <>
Rakiah

22

La generalmente excelente Biblioteca de colecciones genéricas C5 tiene varias estructuras de datos diferentes basadas en árboles, incluidos conjuntos, bolsas y diccionarios. El código fuente está disponible si desea estudiar los detalles de su implementación. (He usado colecciones C5 en código de producción con buenos resultados, aunque no he usado ninguna de las estructuras de árbol específicamente).


77
No sé si tal vez las cosas han cambiado, pero en este momento el libro está disponible gratuitamente para descargar como PDF desde el sitio C5.
Oskar

44
La falta de documentación ya no es una preocupación, ya que hay un pdf de 272 páginas que complementa la biblioteca ... No puedo comentar sobre la calidad del código, pero a juzgar por la calidad del documento, ¡estoy deseando profundizar en esto esta noche!
Florian Doyon el

2
Por lo que entiendo, esta biblioteca C5 no tiene árboles en absoluto, sino solo algunas estructuras de datos derivadas de árboles.
roim

10

Ver http://quickgraph.codeplex.com/

QuickGraph proporciona estructuras de datos y algoritmos de gráficos genéricos dirigidos / no dirigidos para .Net 2.0 y versiones posteriores. QuickGraph viene con algoritmos tales como búsqueda de profundidad, búsqueda de respiración, búsqueda A *, ruta más corta, ruta k-más corta, flujo máximo, árbol de expansión mínimo, antepasados ​​menos comunes, etc. QuickGraph admite MSAGL, GLEE y Graphviz para renderizar los gráficos, la serialización a GraphML, etc.



7

Tengo una pequeña extensión a las soluciones.

Usando una declaración genérica recursiva y una subclase derivada, puede concentrarse mejor en su objetivo real.

Tenga en cuenta que es diferente de una implementación no genérica, no necesita emitir 'nodo' en 'NodeWorker'.

Aquí está mi ejemplo:

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);  
}  

¿Qué es la profundidad y dónde y cómo se obtiene?
PositiveGuy

@ WeDoTDD.com observando su clase, verá que Traverse lo declara como 0 para comenzar en el nodo raíz, luego usa el método transversal agregando a ese int cada iteración.
Edward

¿Cómo buscarías en todo el árbol un nodo en particular?
matepm

6

Aquí está el mío:

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;
    }
}

Salida:

Fastfood
   Pizza
      Margherita
      Marinara
   Burger
      Cheese burger
      Chili burger
      Rice burger

4

Prueba esta simple muestra.

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

Creo una clase Node que podría ser útil para otras personas. La clase tiene propiedades como:

  • Niños
  • Antepasados
  • Descendientes
  • Hermanos
  • Nivel del nodo
  • Padre
  • Raíz
  • Etc.

También existe la posibilidad de convertir una lista plana de elementos con un Id y un ParentId en un árbol. Los nodos tienen una referencia tanto a los hijos como a los padres, por lo que los nodos iterativos son bastante rápidos.


2

Como no se menciona, me gustaría llamar la atención sobre la base de código .net ahora lanzada: específicamente el código para un SortedSetque implementa un Árbol Rojo-Negro:

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

Sin embargo, esta es una estructura de árbol equilibrada. Entonces, mi respuesta es más una referencia a lo que creo que es la única estructura de árbol nativa en la biblioteca principal de .net.


2

He completado el código que @Berezh ha compartido.

  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;
        }
    }

Buen diseño. Sin embargo, no estoy seguro de si un nodo 'es' una secuencia de su nodo hijo. Consideraría lo siguiente: un nodo 'tiene' cero o más nodos secundarios, por lo que un nodo no se deriva de una secuencia de nodos secundarios, pero es una agregación (¿composición?) De sus nodos secundarios
Harald Coppoolse

2

Aquí hay un árbol

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;
    }
}

Incluso puedes usar inicializadores:

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

1

La mayoría de los árboles están formados por los datos que está procesando.

Digamos que tiene una personclase que incluye detalles de alguien parents, ¿preferiría tener la estructura de árbol como parte de su "clase de dominio", o usar una clase de árbol separada que contuviera enlaces a sus objetos personales? Piense en una operación simple como obtener todo el grandchildrena person, ¿debería este código estar en la person clase, o el usuario de la personclase debe saber sobre una clase de árbol separada?

Otro ejemplo es un árbol de análisis en un compilador ...

Lo que muestran estos dos ejemplos es que el concepto de árbol es parte del dominio de los datos y el uso de un árbol de propósito general separado al menos duplica el número de objetos que se crean y hace que la API sea más difícil de programar nuevamente.

Lo que queremos es una forma de reutilizar las operaciones de árbol estándar, sin tener que volver a implementarlas para todos los árboles, al mismo tiempo, sin tener que usar una clase de árbol estándar. Boost ha intentado resolver este tipo de problema para C ++, pero aún no he visto ningún efecto para .NET adaptarse.


@ Puchacz, lo siento, tengo 15 años sin datos en C ++, eche un vistazo a Boost and Templates, después de algunos pocos estudios, puede que los entienda. ¡El poder tiene altos costos de aprendizaje!
Ian Ringrose

1

He agregado una solución completa y un ejemplo usando la clase NTree anterior, también agregué el método "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;
        }
    }

utilizando

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

¿La poligonal debería ser un método estático? Parece muy incómodo como un método de instancia que se pasa a sí mismo
Sinaesthetic

0

Aquí está mi implementación de 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

Si va a mostrar este árbol en la GUI, puede usar TreeView y TreeNode . (Supongo que técnicamente puede crear un TreeNode sin ponerlo en una GUI, pero tiene más sobrecarga que una simple implementación de TreeNode propia).


-4

En caso de que necesite una implementación de estructura de datos de árbol enraizada que use menos memoria, puede escribir su clase de nodo de la siguiente manera (implementación de 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
Publicar código C ++ en una pregunta específica para C # no es la mejor idea, Jake. Especialmente uno que incluye punteros. Sabes que los punteros están siendo perseguidos sin piedad en C #, ¿verdad? : p
ThunderGr

2
@ThunderGr eso no es justo. Responder en C # hubiera sido mejor, pero los punteros de C ++ pueden entender esos punteros de C ++ como referencias (son menos seguros, ok). Después de que David Boike, Aaron Gage, Ronnie Overby, Grzegorz Dev, Berezh y Erik Nagel hayan sugerido básicamente la misma estructura de datos con pequeñas diferencias solo en la expresión, Jake sugirió desglosar la lista vinculada produciendo estructuras más simples con un solo tipo de nodo y navegabilidad entre hermanos. No exprese su disgusto por C ++ votando negativamente una respuesta constructiva.
migle

3
@migle No voté a favor la respuesta (tampoco voté a favor). Y no me disgusta C ++. Vi que la respuesta fue rechazada sin que nadie sugiriera nada a Jake sobre por qué y cómo mejoraría su respuesta. No se trata de "ser mejor". La pregunta está etiquetada solo para C #. No se recomienda publicar respuestas en otro idioma que no sea la etiqueta y algunas personas votarán negativamente.
ThunderGr
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.