Melhor maneira de comparar dois objetos complexos


112

Tenho dois objetos complexos como Object1e Object2. Eles têm cerca de 5 níveis de objetos filhos.

Preciso do método mais rápido para dizer se eles são iguais ou não.

Como isso pode ser feito no C # 4.0?

Respostas:


101

Implemente IEquatable<T>(normalmente em conjunto com a substituição dos métodos Object.Equalse herdados Object.GetHashCode) em todos os seus tipos personalizados. No caso de tipos compostos, invoque o Equalsmétodo dos tipos contidos nos tipos contidos . Para coleções contidas, use o SequenceEqualmétodo de extensão, que chama internamente IEquatable<T>.Equalsou Object.Equalsem cada elemento. Essa abordagem obviamente exigirá que você estenda as definições de seus tipos, mas seus resultados são mais rápidos do que quaisquer soluções genéricas envolvendo serialização.

Edit : Aqui está um exemplo inventado com três níveis de aninhamento.

Para tipos de valor, normalmente você pode apenas chamar seu Equalsmétodo. Mesmo que os campos ou propriedades nunca tenham sido atribuídos explicitamente, eles ainda terão um valor padrão.

Para tipos de referência, você deve primeiro chamar ReferenceEquals, que verifica a igualdade de referência - isso serviria como um aumento de eficiência quando você estiver fazendo referência ao mesmo objeto. Ele também lidaria com casos em que ambas as referências são nulas. Se essa verificação falhar, confirme se o campo ou propriedade da instância não é nulo (para evitar NullReferenceException) e chame seu Equalsmétodo. Uma vez que nossos membros são digitados corretamente, o IEquatable<T>.Equalsmétodo é chamado diretamente, ignorando o Object.Equalsmétodo sobrescrito (cuja execução seria um pouco mais lenta devido ao tipo de conversão).

Quando você substitui Object.Equals, também é esperado que você substitua Object.GetHashCode; Eu não fiz isso abaixo por uma questão de concisão.

public class Person : IEquatable<Person>
{
    public int Age { get; set; }
    public string FirstName { get; set; }
    public Address Address { get; set; }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as Person);
    }

    public bool Equals(Person other)
    {
        if (other == null)
            return false;

        return this.Age.Equals(other.Age) &&
            (
                object.ReferenceEquals(this.FirstName, other.FirstName) ||
                this.FirstName != null &&
                this.FirstName.Equals(other.FirstName)
            ) &&
            (
                object.ReferenceEquals(this.Address, other.Address) ||
                this.Address != null &&
                this.Address.Equals(other.Address)
            );
    }
}

public class Address : IEquatable<Address>
{
    public int HouseNo { get; set; }
    public string Street { get; set; }
    public City City { get; set; }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as Address);
    }

    public bool Equals(Address other)
    {
        if (other == null)
            return false;

        return this.HouseNo.Equals(other.HouseNo) &&
            (
                object.ReferenceEquals(this.Street, other.Street) ||
                this.Street != null &&
                this.Street.Equals(other.Street)
            ) &&
            (
                object.ReferenceEquals(this.City, other.City) ||
                this.City != null &&
                this.City.Equals(other.City)
            );
    }
}

public class City : IEquatable<City>
{
    public string Name { get; set; }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as City);
    }

    public bool Equals(City other)
    {
        if (other == null)
            return false;

        return
            object.ReferenceEquals(this.Name, other.Name) ||
            this.Name != null &&
            this.Name.Equals(other.Name);
    }
}

Atualização : Esta resposta foi escrita há vários anos. Desde então, comecei a me afastar da implementação IEquality<T>de tipos mutáveis ​​para esses cenários. Existem duas noções de igualdade: identidade e equivalência . Em um nível de representação de memória, eles são popularmente distinguidos como “igualdade de referência” e “igualdade de valor” (consulte Comparações de igualdade ). No entanto, a mesma distinção também pode ser aplicada em um nível de domínio. Suponha que sua Personclasse tenha uma PersonIdpropriedade, única por pessoa distinta do mundo real. Devem dois objetos com os mesmos valores, PersonIdmas diferentes Age, ser considerados iguais ou diferentes? A resposta acima assume que se busca a equivalência. No entanto, existem muitos usos doIEquality<T>interface, como coleções, que assumem que tais implementações fornecem identidade . Por exemplo, se você estiver preenchendo um HashSet<T>, normalmente esperaria que uma TryGetValue(T,T)chamada retornasse elementos existentes que compartilham apenas a identidade de seu argumento, não necessariamente elementos equivalentes cujo conteúdo é completamente o mesmo. Esta noção é reforçada pelas notas sobre GetHashCode:

Em geral, para tipos de referência mutáveis, você deve substituir GetHashCode()apenas se:

  • Você pode calcular o código hash de campos que não são mutáveis; ou
  • Você pode garantir que o código hash de um objeto mutável não seja alterado enquanto o objeto estiver contido em uma coleção que depende de seu código hash.

Eu obtenho este objeto por meio de serviços RIA ... Posso usar IEquatable <Foo> para esses objetos e obtê-lo no cliente WPF?
Desenvolvedor

1
Quer dizer que as classes são geradas automaticamente? Eu não usei os serviços RIA, mas presumo que quaisquer classes geradas seriam declaradas como partial- nesse caso, sim, você pode implementar seu Equalsmétodo por meio de uma declaração de classe parcial adicionada manualmente que faz referência a campos / propriedades do gerado automaticamente 1.
Douglas

E se "Endereço" for na verdade "Endereço [] endereços", como seria implementado?
guiomie

2
Você poderia chamar o LINQ Enumerable.SequenceEqualmétodo nas matrizes: this.Addresses.SequenceEqual(other.Addresses). Isso chamaria internamente seu Address.Equalsmétodo para cada par de endereços correspondentes, visto que a Addressclasse implementa a IEquatable<Address>interface.
Douglas

2
Outra categoria de comparação que um desenvolvedor pode verificar é "WorksLike". Para mim, isso significa que, embora duas instâncias possam ter alguns valores de propriedade desiguais, o programa produzirá o mesmo resultado do processamento das duas instâncias.
John Kurtz

95

Serialize os dois objetos e compare as strings resultantes


1
Não vejo por que haveria. A serialização é normalmente um processo otimizado e você precisa acessar o valor de cada propriedade em qualquer caso.
JoelFan

5
Há um custo enorme. Você está gerando um fluxo de dados, acrescentando strings e, em seguida, testando a igualdade das strings. Ordens de magnitude, apenas nisso. Sem mencionar que a serialização usará reflexão por padrão.
Jerome Haltom

2
Fluxo de dados não é grande coisa, não vejo por que você precisaria anexar strings ... testar a igualdade de strings é uma das operações mais otimizadas lá fora ... você pode ter um ponto com reflexão ... mas em toda a serialização não será "ordens de magnitude" pior do que outros métodos. Você deve fazer benchmarks se suspeitar de problemas de desempenho ... Não tive problemas de desempenho com este método
JoelFan

12
Eu sou +1isso simplesmente porque nunca pensei em fazer uma comparação de igualdade baseada em valores dessa maneira. É bom e simples. Seria legal ver alguns benchmarks com este código.
Thomas

1
Essa não é uma boa solução, pois ambas as serializações podem dar errado de maneira semelhante. Por exemplo, algumas propriedades do objeto de origem podem não ter sido serializadas e quando desserializadas serão definidas como nulas no objeto de destino. Nesse caso, seu teste que compara strings passará, mas os dois objetos não são iguais!
stackMeUp

35

Você pode usar o método de extensão, recursão para resolver este problema:

public static bool DeepCompare(this object obj, object another)
{     
  if (ReferenceEquals(obj, another)) return true;
  if ((obj == null) || (another == null)) return false;
  //Compare two object's class, return false if they are difference
  if (obj.GetType() != another.GetType()) return false;

  var result = true;
  //Get all properties of obj
  //And compare each other
  foreach (var property in obj.GetType().GetProperties())
  {
      var objValue = property.GetValue(obj);
      var anotherValue = property.GetValue(another);
      if (!objValue.Equals(anotherValue)) result = false;
  }

  return result;
 }

public static bool CompareEx(this object obj, object another)
{
 if (ReferenceEquals(obj, another)) return true;
 if ((obj == null) || (another == null)) return false;
 if (obj.GetType() != another.GetType()) return false;

 //properties: int, double, DateTime, etc, not class
 if (!obj.GetType().IsClass) return obj.Equals(another);

 var result = true;
 foreach (var property in obj.GetType().GetProperties())
 {
    var objValue = property.GetValue(obj);
    var anotherValue = property.GetValue(another);
    //Recursion
    if (!objValue.DeepCompare(anotherValue))   result = false;
 }
 return result;
}

ou compare usando Json (se o objeto for muito complexo) Você pode usar Newtonsoft.Json:

public static bool JsonCompare(this object obj, object another)
{
  if (ReferenceEquals(obj, another)) return true;
  if ((obj == null) || (another == null)) return false;
  if (obj.GetType() != another.GetType()) return false;

  var objJson = JsonConvert.SerializeObject(obj);
  var anotherJson = JsonConvert.SerializeObject(another);

  return objJson == anotherJson;
}

1
A primeira solução é ótima! Eu gosto que você não precise serializar JSON ou implementar adicionar qualquer código aos próprios objetos. Adequado quando você está apenas comparando para testes de unidade. Posso sugerir adicionar uma comparação simples no caso de objValue e anotherValue serem ambos iguais a nulo? Isso evitará que uma NullReferenceException seja lançada ao tentar fazer null.Equals () // ReSharper desabilitar uma vez RedundantJumpStatement if (objValue == anotherValue) continue; // proteção de referência nula else if (! objValue.Equals (anotherValue)) Fail (esperado, real);
Mark Conway,

3
Existe algum motivo para usar em DeepComparevez de simplesmente chamar CompareExrecursivamente?
apostolov

3
Isso pode comparar toda a estrutura desnecessariamente. Substituir resultpor return falseo tornaria mais eficiente.
Tim Sylvester

24

Se você não quiser implementar IEquatable, você pode sempre usar o Reflection para comparar todas as propriedades: - se eles forem do tipo de valor, apenas compare-os -se forem do tipo de referência, chame a função recursivamente para comparar suas propriedades "internas" .

Não estou pensando em performace, mas sim em simplicidade. Depende, entretanto, do design exato de seus objetos. Pode ficar complicado dependendo da forma dos seus objetos (por exemplo, se houver dependências cíclicas entre propriedades). No entanto, existem várias soluções que você pode usar, como esta:

Outra opção é serializar o objeto como texto, por exemplo, usando JSON.NET e comparando o resultado da serialização. (JSON.NET pode lidar com dependências cíclicas entre propriedades).

Não sei se por mais rápido você quer dizer a maneira mais rápida de implementá-lo ou um código que roda rápido. Você não deve otimizar antes de saber se é necessário. Otimização prematura é a raiz de todo o mal


1
Eu dificilmente acho que uma IEquatable<T>implementação se qualifica como um caso de otimização prematura. A reflexão será drasticamente mais lenta. A implementação padrão de Equalspara tipos de valor customizado usa reflexão; A própria Microsoft recomenda substituí-lo para desempenho: “Substitua o Equalsmétodo de um tipo específico para melhorar o desempenho do método e representar mais de perto o conceito de igualdade para o tipo.”
Douglas

1
Depende de quantas vezes ele vai executar o método equals: 1, 10, 100, 100, um milhão? Isso fará uma grande diferença. Se ele puder usar uma solução genérica sem implementar nada, ele poupará um tempo precioso. Se for muito lento, então é hora de implementar IEquatable (e talvez até tentar fazer um GetHashCode inteligente ou em cache). No que diz respeito à velocidade do Reflection, devo concordar que é mais lento ... ou muito mais lento, dependendo de como você o faz ( ou seja, reutilizando Tipos PropertyInfos e assim por diante, ou não).
JotaBe

@ Worthy7 Sim. Por favor, veja o conteúdo do projeto. Os testes são uma boa maneira de documentar por exemplo. Mas, melhor do que isso, se você procurar por ele, encontrará um arquivo de ajuda .chm. Portanto, este projeto tem uma documentação muito melhor do que a maioria dos projetos.
JotaBe

Desculpe, você está certo, eu perdi totalmente a guia "wiki". Estou acostumado com todo mundo escrevendo coisas no readme.
Worthy7

9

Serialize ambos os objetos e compare as strings resultantes por @JoelFan

Então, para fazer isso, crie uma classe estática como essa e use Extensões para estender TODOS os objetos (para que você possa passar qualquer tipo de objeto, coleção, etc. para o método)

using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;

public static class MySerializer
{
    public static string Serialize(this object obj)
    {
        var serializer = new DataContractJsonSerializer(obj.GetType());
        using (var ms = new MemoryStream())
        {
            serializer.WriteObject(ms, obj);
            return Encoding.Default.GetString(ms.ToArray());
        }
    }
}

Depois de fazer referência a essa classe estática em qualquer outro arquivo, você pode fazer isso:

Person p = new Person { Firstname = "Jason", LastName = "Argonauts" };
Person p2 = new Person { Firstname = "Jason", LastName = "Argonaut" };
//assuming you have already created a class person!
string personString = p.Serialize();
string person2String = p2.Serialize();

Agora você pode simplesmente usar .Equals para compará-los. Eu uso isso para verificar se os objetos estão em coleções também. Funciona muito bem.


E se o conteúdo dos objetos forem matrizes de números de ponto flutuante. É muito ineficiente convertê-los em strings, e a conversão está sujeita às transformações definidas em CultrureInfo. Isso só funcionaria se os dados internos fossem principalmente strings e inteiros. Caso contrário, seria um desastre.
John Alexiou

3
E se um novo diretor dissesse para você remover o C # e substituí-lo pelo Python. Como desenvolvedores, precisamos aprender que as questões e se devem parar em algum lugar. Resolva o problema para o próximo. Se você SEMPRE tiver tempo, volte para ele ...
ozzy432836

2
Python é mais parecido com o MATLAB em sintaxe e uso. Deve haver uma razão muito boa para mudar de uma linguagem segura de tipo estático para um script mishmash como o python.
John Alexiou

5

Vou assumir que você não está se referindo literalmente aos mesmos objetos

Object1 == Object2

Você pode estar pensando em fazer uma comparação de memória entre os dois

memcmp(Object1, Object2, sizeof(Object.GetType())

Mas isso nem mesmo é código real em c # :). Como todos os seus dados provavelmente são criados no heap, a memória não é contígua e você não pode simplesmente comparar a igualdade de dois objetos de maneira agnóstica. Você terá que comparar cada valor, um de cada vez, de uma forma personalizada.

Considere adicionar a IEquatable<T>interface à sua classe e definir um Equalsmétodo personalizado para o seu tipo. Então, nesse método, teste manualmente cada valor. Adicione IEquatable<T>novamente os tipos fechados, se possível, e repita o processo.

class Foo : IEquatable<Foo>
{
  public bool Equals(Foo other)
  {
    /* check all the values */
    return false;
  }
}


3

Encontrei esta função abaixo para comparar objetos.

 static bool Compare<T>(T Object1, T object2)
 {
      //Get the type of the object
      Type type = typeof(T);

      //return false if any of the object is false
      if (object.Equals(Object1, default(T)) || object.Equals(object2, default(T)))
         return false;

     //Loop through each properties inside class and get values for the property from both the objects and compare
     foreach (System.Reflection.PropertyInfo property in type.GetProperties())
     {
          if (property.Name != "ExtensionData")
          {
              string Object1Value = string.Empty;
              string Object2Value = string.Empty;
              if (type.GetProperty(property.Name).GetValue(Object1, null) != null)
                    Object1Value = type.GetProperty(property.Name).GetValue(Object1, null).ToString();
              if (type.GetProperty(property.Name).GetValue(object2, null) != null)
                    Object2Value = type.GetProperty(property.Name).GetValue(object2, null).ToString();
              if (Object1Value.Trim() != Object2Value.Trim())
              {
                  return false;
              }
          }
     }
     return true;
 }

Estou usando e está funcionando bem para mim.


1
O primeiro ifsignifica aquilo Compare(null, null) == falseque não é o que eu esperava.
Tim Sylvester

3

Com base em algumas respostas já fornecidas aqui, decidi apoiar principalmente a resposta de JoelFan . Eu amo métodos de extensão e eles têm funcionado muito bem para mim quando nenhuma das outras soluções os usaria para comparar minhas classes complexas.

Métodos de Extensão

using System.IO;
using System.Xml.Serialization;

static class ObjectHelpers
{
    public static string SerializeObject<T>(this T toSerialize)
    {
        XmlSerializer xmlSerializer = new XmlSerializer(toSerialize.GetType());

        using (StringWriter textWriter = new StringWriter())
        {
            xmlSerializer.Serialize(textWriter, toSerialize);
            return textWriter.ToString();
        }
    }

    public static bool EqualTo(this object obj, object toCompare)
    {
        if (obj.SerializeObject() == toCompare.SerializeObject())
            return true;
        else
            return false;
    }

    public static bool IsBlank<T>(this T obj) where T: new()
    {
        T blank = new T();
        T newObj = ((T)obj);

        if (newObj.SerializeObject() == blank.SerializeObject())
            return true;
        else
            return false;
    }

}

Exemplos de uso

if (record.IsBlank())
    throw new Exception("Record found is blank.");

if (record.EqualTo(new record()))
    throw new Exception("Record found is blank.");

2

Eu diria que:

Object1.Equals(Object2)

seria o que você está procurando. Isso se você estiver procurando ver se os objetos são os mesmos, que é o que parece estar perguntando.

Se você deseja verificar se todos os objetos filhos são iguais, execute-os em um loop com o Equals()método.


2
Se e somente se eles fornecerem uma sobrecarga de igualdade sem referência de Equals.
user7116

Cada classe deve implementar sua própria forma de comparação. Se as classes do autor não tiverem substituições para o método Equals (), elas usarão o método básico da classe System.Object (), o que levará a erros na lógica.
Dima

2
public class GetObjectsComparison
{
    public object FirstObject, SecondObject;
    public BindingFlags BindingFlagsConditions= BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
}
public struct SetObjectsComparison
{
    public FieldInfo SecondObjectFieldInfo;
    public dynamic FirstObjectFieldInfoValue, SecondObjectFieldInfoValue;
    public bool ErrorFound;
    public GetObjectsComparison GetObjectsComparison;
}
private static bool ObjectsComparison(GetObjectsComparison GetObjectsComparison)
{
    GetObjectsComparison FunctionGet = GetObjectsComparison;
    SetObjectsComparison FunctionSet = new SetObjectsComparison();
    if (FunctionSet.ErrorFound==false)
        foreach (FieldInfo FirstObjectFieldInfo in FunctionGet.FirstObject.GetType().GetFields(FunctionGet.BindingFlagsConditions))
        {
            FunctionSet.SecondObjectFieldInfo =
            FunctionGet.SecondObject.GetType().GetField(FirstObjectFieldInfo.Name, FunctionGet.BindingFlagsConditions);

            FunctionSet.FirstObjectFieldInfoValue = FirstObjectFieldInfo.GetValue(FunctionGet.FirstObject);
            FunctionSet.SecondObjectFieldInfoValue = FunctionSet.SecondObjectFieldInfo.GetValue(FunctionGet.SecondObject);
            if (FirstObjectFieldInfo.FieldType.IsNested)
            {
                FunctionSet.GetObjectsComparison =
                new GetObjectsComparison()
                {
                    FirstObject = FunctionSet.FirstObjectFieldInfoValue
                    ,
                    SecondObject = FunctionSet.SecondObjectFieldInfoValue
                };

                if (!ObjectsComparison(FunctionSet.GetObjectsComparison))
                {
                    FunctionSet.ErrorFound = true;
                    break;
                }
            }
            else if (FunctionSet.FirstObjectFieldInfoValue != FunctionSet.SecondObjectFieldInfoValue)
            {
                FunctionSet.ErrorFound = true;
                break;
            }
        }
    return !FunctionSet.ErrorFound;
}

Usando os princípios da recursão.
matan justme

1

Uma maneira de fazer isso seria substituir Equals()em cada tipo envolvido. Por exemplo, seu objeto de nível superior seria substituído Equals()para chamar o Equals()método de todos os 5 objetos filho. Esses objetos também devem ser substituídos Equals(), assumindo que sejam objetos personalizados, e assim por diante, até que toda a hierarquia possa ser comparada apenas executando uma verificação de igualdade nos objetos de nível superior.



1

Graças ao exemplo de Jonathan. Eu o expandi para todos os casos (arrays, listas, dicionários, tipos primitivos).

Esta é uma comparação sem serialização e não requer a implementação de nenhuma interface para objetos comparados.

        /// <summary>Returns description of difference or empty value if equal</summary>
        public static string Compare(object obj1, object obj2, string path = "")
        {
            string path1 = string.IsNullOrEmpty(path) ? "" : path + ": ";
            if (obj1 == null && obj2 != null)
                return path1 + "null != not null";
            else if (obj2 == null && obj1 != null)
                return path1 + "not null != null";
            else if (obj1 == null && obj2 == null)
                return null;

            if (!obj1.GetType().Equals(obj2.GetType()))
                return "different types: " + obj1.GetType() + " and " + obj2.GetType();

            Type type = obj1.GetType();
            if (path == "")
                path = type.Name;

            if (type.IsPrimitive || typeof(string).Equals(type))
            {
                if (!obj1.Equals(obj2))
                    return path1 + "'" + obj1 + "' != '" + obj2 + "'";
                return null;
            }
            if (type.IsArray)
            {
                Array first = obj1 as Array;
                Array second = obj2 as Array;
                if (first.Length != second.Length)
                    return path1 + "array size differs (" + first.Length + " vs " + second.Length + ")";

                var en = first.GetEnumerator();
                int i = 0;
                while (en.MoveNext())
                {
                    string res = Compare(en.Current, second.GetValue(i), path);
                    if (res != null)
                        return res + " (Index " + i + ")";
                    i++;
                }
            }
            else if (typeof(System.Collections.IEnumerable).IsAssignableFrom(type))
            {
                System.Collections.IEnumerable first = obj1 as System.Collections.IEnumerable;
                System.Collections.IEnumerable second = obj2 as System.Collections.IEnumerable;

                var en = first.GetEnumerator();
                var en2 = second.GetEnumerator();
                int i = 0;
                while (en.MoveNext())
                {
                    if (!en2.MoveNext())
                        return path + ": enumerable size differs";

                    string res = Compare(en.Current, en2.Current, path);
                    if (res != null)
                        return res + " (Index " + i + ")";
                    i++;
                }
            }
            else
            {
                foreach (PropertyInfo pi in type.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public))
                {
                    try
                    {
                        var val = pi.GetValue(obj1);
                        var tval = pi.GetValue(obj2);
                        if (path.EndsWith("." + pi.Name))
                            return null;
                        var pathNew = (path.Length == 0 ? "" : path + ".") + pi.Name;
                        string res = Compare(val, tval, pathNew);
                        if (res != null)
                            return res;
                    }
                    catch (TargetParameterCountException)
                    {
                        //index property
                    }
                }
                foreach (FieldInfo fi in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public))
                {
                    var val = fi.GetValue(obj1);
                    var tval = fi.GetValue(obj2);
                    if (path.EndsWith("." + fi.Name))
                        return null;
                    var pathNew = (path.Length == 0 ? "" : path + ".") + fi.Name;
                    string res = Compare(val, tval, pathNew);
                    if (res != null)
                        return res;
                }
            }
            return null;
        }

Para cópia fácil do repositório de código criado


1

Agora você pode usar json.net. Basta acessar o Nuget e instalá-lo.

E você pode fazer algo assim:

    public bool Equals(SamplesItem sampleToCompare)
    {
        string myself = JsonConvert.SerializeObject(this);
        string other = JsonConvert.SerializeObject(sampleToCompare);

        return myself == other;
    }

Você poderia talvez fazer um método de extensão para o objeto se quiser ficar mais sofisticado. Observe que isso só compara as propriedades públicas. E se você quiser ignorar uma propriedade pública ao fazer a comparação, poderá usar o atributo [JsonIgnore].


Se você tiver listas em seus objetos e esses tiverem listas, tentar atravessar os dois objetos será um pesadelo. Se você serializar ambos e depois comparar, não terá que lidar com esse cenário.
ashlar64 01 de

Se seu objeto complexo contiver um Dicionário, não acredito que o serializador .net possa serializá-lo. O serializador Json pode.
ashlar64 01 de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.