Лучший способ сравнить два сложных объекта


112

У меня есть два сложных объекта вроде Object1и Object2. У них есть около 5 уровней дочерних объектов.

Мне нужен самый быстрый способ сказать, совпадают они или нет.

Как это можно было сделать в C # 4.0?

Ответы:


101

Реализовать IEquatable<T>( как правило , в сочетании с переопределение унаследованных Object.Equalsи Object.GetHashCodeметоды) на всех пользовательских типов. В случае составных типов вызовите метод содержащихся типов Equalsвнутри содержащих типы. Для содержащихся коллекций используйте SequenceEqualметод расширения, который внутренне вызывает IEquatable<T>.Equalsили Object.Equalsдля каждого элемента. Этот подход, очевидно, потребует от вас расширения определений ваших типов, но его результаты быстрее, чем любые общие решения, включающие сериализацию.

Изменить : вот надуманный пример с тремя уровнями вложенности.

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

Для ссылочных типов вы должны сначала вызвать ReferenceEquals, который проверяет ссылочное равенство - это послужит повышению эффективности, когда вы случайно ссылаетесь на один и тот же объект. Он также будет обрабатывать случаи, когда обе ссылки равны нулю. Если эта проверка не удалась, убедитесь, что поле или свойство вашего экземпляра не равно нулю (во избежание NullReferenceException) и вызовите его Equalsметод. Поскольку наши члены набраны правильно, IEquatable<T>.Equalsметод вызывается напрямую, минуя переопределенныйObject.Equals метод (выполнение которого будет немного медленнее из-за приведения типа).

Когда вы переопределяете Object.Equals, вы также должны переопределить Object.GetHashCode; Я не делал этого ниже для краткости.

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

Обновление : этот ответ был написан несколько лет назад. С тех пор я начал отказываться от реализации IEquality<T>изменяемых типов для таких сценариев. Есть два понятия равенства: тождество и эквивалентность . На уровне представления памяти их обычно различают как «эталонное равенство» и «равенство значений» (см. Сравнение равенства ). Однако такое же различие может применяться и на уровне домена. Предположим, что у вашего Personкласса есть PersonIdсвойство, уникальное для конкретного человека в реальном мире. Следует ли считать два объекта с одинаковыми, PersonIdно разными Ageзначениями одинаковыми или разными? В приведенном выше ответе предполагается, что требуется эквивалентность. Однако есть много вариантов использованияIEquality<T> интерфейсом, например коллекции, предполагают, что такие реализации обеспечивают идентификацию . Например, если вы заполняетеHashSet<T>, вы обычно ожидаете, что TryGetValue(T,T)вызов вернет существующие элементы, которые разделяют только идентичность вашего аргумента, не обязательно эквивалентные элементы, содержимое которых полностью одинаково. Это понятие подтверждается примечаниями к GetHashCode:

В общем, для изменяемых ссылочных типов следует переопределить, GetHashCode()только если:

  • Вы можете вычислить хэш-код из неизменяемых полей; или
  • Вы можете гарантировать, что хэш-код изменяемого объекта не изменится, пока объект содержится в коллекции, которая полагается на его хэш-код.

Я получаю этот объект через службы RIA ... Могу ли я использовать IEquatable <Foo> для этих объектов и получить его под клиентом WPF?
— Разработчик

1
Вы имеете в виду, что классы создаются автоматически? Я не использовал службы RIA, но предполагаю, что любые сгенерированные классы будут объявлены как partial- и в этом случае, да, вы можете реализовать их Equalsметод через добавленное вручную объявление частичного класса, которое ссылается на поля / свойства из автоматически сгенерированного один.
— Дуглас

Что, если «Адресный адрес» на самом деле является «Адрес [] адрес», как бы это было реализовано?
— guiomie

2
Вы могли бы назвать LINQ Enumerable.SequenceEqualметод на массивы: this.Addresses.SequenceEqual(other.Addresses). Это внутренне вызовет ваш Address.Equalsметод для каждой пары соответствующих адресов, учитывая, что Addressкласс реализует IEquatable<Address>интерфейс.
— Дуглас

2
Другая категория сравнения, которую может проверить разработчик, - это «WorksLike». Для меня это означает, что даже если у двух экземпляров могут быть разные значения свойств, программа выдаст одинаковый результат при обработке двух экземпляров.
— Джон Курц

94

Сериализуйте оба объекта и сравните полученные строки


1
Я не понимаю, почему это могло быть. Сериализация обычно является оптимизированным процессом, и вам в любом случае необходимо получить доступ к значению каждого свойства.
— JoelFan

5
Это огромная цена. Вы генерируете поток данных, добавляете строки, а затем проверяете равенство строк. По порядку величины, как раз в этом. Не говоря уже о том, что сериализация по умолчанию будет использовать отражение.
— Джером Халтом

2
Поток данных не имеет большого значения, я не понимаю, зачем вам нужно добавлять строки ... проверка равенства строк - одна из наиболее оптимизированных операций ... у вас может быть точка с отражением ... но на вся сериализация не будет «на порядки» хуже других методов. Вам следует выполнить тесты, если вы подозреваете проблемы с производительностью ... У меня не было проблем с производительностью с помощью этого метода
— JoelFan

12
Я занимаюсь +1этим просто потому, что никогда не думал о проведении подобного сравнения равенства на основе значений. Это красиво и просто. Было бы неплохо увидеть тесты с этим кодом.
— Thomas

1
Это не лучшее решение, поскольку обе сериализации могут пойти не так, как надо. Например, некоторые свойства исходного объекта могли не быть сериализованы, и при десериализации в целевом объекте будет установлено значение null. В таком случае ваш тест, сравнивающий строки, пройдет успешно, но на самом деле оба объекта не совпадают!
— stackMeUp

35

Вы можете использовать метод расширения, рекурсию, чтобы решить эту проблему:

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

или сравните с помощью Json (если объект очень сложный) Вы можете использовать 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
Первое решение отличное! Мне нравится, что вам не нужно сериализовать json или реализовывать добавление кода к самим объектам. Подходит, когда вы просто сравниваете для юнит-тестов. Могу я предложить добавить простое сравнение в случае, если objValue и anotherValue равны null? Это предотвратит возникновение исключения NullReferenceException при попытке выполнить null.Equals () // ReSharper отключит один раз RedundantJumpStatement if (objValue == anotherValue) continue; // защита нулевой ссылки else if (! objValue.Equals (anotherValue)) Fail (ожидаемый, фактический);
— Марк Конвей

3
Есть ли причина использовать DeepCompareвместо простого CompareExрекурсивного вызова ?
— апостолов

3
Это может привести к ненужному сравнению всей конструкции. Замена resultна return falseсделает его более эффективным.
— Тим Сильвестр

24

Если вы не хотите реализовывать IEquatable, вы всегда можете использовать Reflection для сравнения всех свойств: - если они тип значения, просто сравните их - если они ссылочного типа, вызовите функцию рекурсивно, чтобы сравнить ее «внутренние» свойства .

Я думаю не о производительности, а о простоте. Однако это зависит от точного дизайна ваших объектов. Это может усложниться в зависимости от формы ваших объектов (например, если есть циклические зависимости между свойствами). Однако есть несколько решений, которые вы можете использовать, например это:

Другой вариант - сериализовать объект как текст, например, с помощью JSON.NET, и сравнить результат сериализации. (JSON.NET может обрабатывать циклические зависимости между свойствами).

Я не знаю, подразумеваете ли вы под «самым быстрым» самый быстрый способ его реализации или код, который работает быстро. Вы не должны оптимизировать, пока не узнаете, нужно ли вам это. Преждевременная оптимизация - корень всех зол


1
Я не думаю, что IEquatable<T>реализация подпадает под преждевременную оптимизацию. Отражение будет значительно медленнее. Реализация по умолчанию Equalsдля настраиваемых типов значений действительно использует отражение; Сами Microsoft рекомендуют переопределить его для повышения производительности: «Переопределите Equalsметод для определенного типа, чтобы повысить производительность метода и более точно представить концепцию равенства для типа».
— Дуглас

1
Это зависит от того, сколько раз он будет использовать метод равенства: 1, 10, 100, 100, миллион? Это будет иметь большое значение. Если он сможет использовать универсальное решение, ничего не реализовав, он сэкономит драгоценное время. Если он слишком медленный, то пора реализовать IEquatable (и, возможно, даже попытаться сделать кешируемый или интеллектуальный GetHashCode). Что касается скорости Reflection, я должен согласиться, что она медленнее ... или намного медленнее, в зависимости от того, как вы это делаете ( т.е. повторное использование типов PropertyInfos и т. д. или нет).
— JotaBe

@ Worthy7 Верно. Пожалуйста, ознакомьтесь с содержанием проекта. Тесты - хороший способ задокументировать ситуацию. Но, что еще лучше, если вы его поищете, вы найдете файл справки .chm. Так что у этого проекта документация намного лучше, чем у большинства проектов.
— JotaBe

Извините, вы правы, я полностью пропустил вкладку "вики". Я привык, что все пишут в ридми.
— Worthy7

9

Сериализуйте оба объекта и сравните полученные строки с помощью @JoelFan

Поэтому для этого создайте статический класс, подобный этому, и используйте расширения для расширения ВСЕХ объектов (чтобы вы могли передать в метод любой тип объекта, коллекции и т. Д.)

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

После ссылки на этот статический класс в любом другом файле вы можете сделать это:

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

Теперь вы можете просто использовать .Equals для их сравнения. Я использую это для проверки, есть ли объекты в коллекциях. Это действительно хорошо работает.


Что делать, если содержимое объектов представляет собой массивы чисел с плавающей запятой. Преобразование их в строки очень неэффективно, и преобразование зависит от преобразований, определенных в CultrureInfo. Это будет работать, только если внутренние данные в основном представляют собой строки и целые числа. Иначе была бы катастрофа.
— Джон Алексиу

3
Что, если бы новый директор сказал вам вырвать C # и заменить его Python. Как разработчики, мы должны понять, что вопросы «а что, если» должны где-то останавливаться. Решите проблему, переходите к следующей. Если у вас КОГДА-нибудь будет время, вернись к нему ...
— ozzy432836

2
Python больше похож на MATLAB по синтаксису и использованию. Должна быть действительно веская причина для перехода от языка, безопасного для статического типа, к мешанине, такой как python.
— Джон Алексиу

5

Я предполагаю, что вы имеете в виду не буквально одни и те же объекты

Object1 == Object2

Возможно, вы думаете о сравнении памяти между двумя

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

Но это даже не настоящий код на C # :). Поскольку все ваши данные, вероятно, созданы в куче, память не является непрерывной, и вы не можете просто сравнивать равенство двух объектов агностическим способом. Вам нужно будет сравнивать каждое значение по отдельности индивидуальным способом.

Подумайте о добавлении IEquatable<T>интерфейса в свой класс и определите собственный Equalsметод для вашего типа. Затем в этом методе вручную проверьте каждое значение. Если можете, добавьте еще IEquatable<T>раз вложенные типы и повторите процесс.

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


3

Я нашел эту функцию ниже для сравнения объектов.

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

Я использую его, и он у меня работает нормально.


1
Первое ifозначает то, Compare(null, null) == falseчего я не ожидал.
— Тим Сильвестр

3

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

Методы расширения

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

}

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

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

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

2

Я бы сказал, что:

Object1.Equals(Object2)

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

Если вы хотите проверить, все ли дочерние объекты одинаковы, запустите их в цикле с этим Equals()методом.


2
Если и только если они обеспечивают перегрузку Equals, не являющуюся ссылочной.
— user7116

Каждый класс должен реализовывать свой способ сравнения. Если авторские классы не имеют переопределений для метода Equals (), они будут использовать базовый метод класса System.Object (), что приведет к ошибкам в логике.
— Дима

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

Использование принципов рекурсии.
— matan justme 09

1

Один из способов сделать это - переопределить Equals()каждый задействованный тип. Например, ваш объект верхнего уровня переопределит Equals()вызов Equals()метода всех 5 дочерних объектов. Все эти объекты также должны переопределяться Equals(), предполагая, что они являются настраиваемыми объектами, и так далее до тех пор, пока всю иерархию можно будет сравнить, просто выполнив проверку равенства для объектов верхнего уровня.



1

Благодаря примеру Джонатана. Я расширил его на все случаи (массивы, списки, словари, примитивные типы).

Это сравнение без сериализации и не требует реализации каких-либо интерфейсов для сравниваемых объектов.

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

Для удобного копирования кода создан репозиторий


1

Теперь вы можете использовать json.net. Просто зайдите на Nuget и установите его.

А можно сделать примерно так:

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

        return myself == other;
    }

Возможно, вы могли бы создать метод расширения для объекта, если хотите стать более привлекательным. Обратите внимание: сравниваются только общедоступные объекты. А если вы хотите игнорировать публичное свойство при сравнении, вы можете использовать атрибут [JsonIgnore].


Если у вас есть списки в ваших объектах, а у них есть списки, то попытка пройти через оба объекта будет кошмаром. Если вы сериализуете оба, а затем сравните, вам не придется иметь дело с этим сценарием.
— ashlar64

Если в вашем сложном объекте есть словарь, я не верю, что сериализатор .net может сериализовать его. Сериализатор Json может.
— ashlar64
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.