Подтвердить что ты не робот

Должен ли я использовать структуру или класс для представления координаты Lat/Lng?

Я работаю с API геокодирования и должен представлять координату возвращенной точки как пару широты/долготы. Однако я не уверен, использовать ли для этого структуру или класс. Моя первоначальная мысль заключалась в том, чтобы использовать структуру, но в С# они, как правило, не одобряются (например, Jon Skeet упоминает в этом ответе, что "я почти никогда не определяю пользовательские структуры" ). Производительность и использование памяти не являются критическими факторами в приложении.

До сих пор я придумал эти две реализации на основе простого интерфейса:

Интерфейс

public interface ILatLng
{
    double Lat { get; }
    double Lng { get; }
}

Внедрение класса LatLng

public class CLatLng : ILatLng
{
    public double Lat { get; private set; }
    public double Lng { get; private set; }

    public CLatLng(double lat, double lng)
    {
        this.Lat = lat;
        this.Lng = lng;
    }

    public override string ToString()
    {
        return String.Format("{0},{1}", this.Lat, this.Lng);
    }

    public override bool Equals(Object obj)
    {
        if (obj == null)
            return false;

        CLatLng latlng = obj as CLatLng;
        if ((Object)latlng == null)
            return false;

        return (this.Lat == latlng.Lat) && (this.Lng == latlng.Lng);
    }

    public bool Equals(CLatLng latlng)
    {
        if ((object)latlng == null)
            return false;

        return (this.Lat == latlng.Lat) && (this.Lng == latlng.Lng);
    }


    public override int GetHashCode()
    {
        return (int)Math.Sqrt(Math.Pow(this.Lat, 2) * Math.Pow(this.Lng, 2));
    }
}

Реализация проекта LatLng

public struct SLatLng : ILatLng
{
    private double _lat;
    private double _lng;

    public double Lat
    {
        get { return _lat; }
        set { _lat = value; }
    }

    public double Lng
    {
        get { return _lng; }
        set { _lng = value; }
    }

    public SLatLng(double lat, double lng)
    {
        this._lat = lat;
        this._lng = lng;
    }

    public override string ToString()
    {
        return String.Format("{0},{1}", this.Lat, this.Lng);
    }
}

Выполняя некоторые тесты, я пришел к следующим выводам:

  • У структуры всегда есть конструктор без параметров, что означает, что вы не можете заставить его быть созданным с помощью конструктора, который ожидает два свойства (для lat и lng), как вы можете, с классом.

  • Структура (являющаяся типом значения) никогда не может быть нулевой, поэтому всегда будет содержать значение. Но при реализации интерфейса вы все равно можете делать такие вещи:

    ILatLng s = new SLatLng(); s = null;

Так ли имеет смысл, чтобы структура использовала интерфейс в этом случае?

  • Если я использую struct, мне нужно переопределить Equals, GetHashCode() и т.д.? Мои тесты показывают, что сравнения работают правильно, не делая этого (в отличие от класса) - так ли это необходимо?

  • Я чувствую себя более "комфортно" с помощью классов, так что лучше всего просто придерживаться их, поскольку я больше знаю, как они себя ведут? Будут ли люди, использующие мой код, путать семантику значений типа, особенно при работе с интерфейсом?

  • В реализации CLatLng выполняется ли переопределение GetHashCode() в порядке? Я "украл" это из этой статьи, поэтому не уверен!

Любая помощь или совет с благодарностью получили!

4b9b3361

Ответ 1

Я не вижу смысла иметь интерфейс для этого, если честно.

Я бы просто создал структуру, но сделаю ее неизменной - изменчивые структуры - действительно плохая идея. Я также использовал бы полные Latitude и Longitude как имена свойств. Что-то вроде этого:

public struct GeoCoordinate
{
    private readonly double latitude;
    private readonly double longitude;

    public double Latitude { get { return latitude; } }
    public double Longitude { get { return longitude; } }

    public GeoCoordinate(double latitude, double longitude)
    {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public override string ToString()
    {
        return string.Format("{0},{1}", Latitude, Longitude);
    }
}

Тогда я бы также реализовал IEquatable<GeoCoordinate> и переопределить Equals и GetHashCode, например.

public override bool Equals(Object other)
{
    return other is GeoCoordinate && Equals((GeoCoordinate) other);
}

public bool Equals(GeoCoordinate other)
{
    return Latitude == other.Latitude && Longitude == other.Longitude;
}

public override int GetHashCode()
{
    return Latitude.GetHashCode() ^ Longitude.GetHashCode();
}

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

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

Ответ 2

Сделайте его структурой для производительности.

  • Эффективность производительности умножается многократно, когда вы обрабатываете, например. массивы этих структур. Следует отметить, что, например, System.Collections.Generic.List правильно обрабатывает unboxed хранения типа элемента в .Net-массивах, поэтому он также применим к универсальным контейнерам.
  • Обратите внимание, что тот факт, что вы не можете иметь конструктор, полностью исключается синтаксисом синтаксиса С# 3.5+:

    new SLatLng { Lat = 1.0, Lng = 2.0 }
    

Стоимость использования интерфейса

Обратите внимание, что добавление интерфейса неизбежно снижает производительность: интерфейсы не могут определять поля, структура без полей вряд ли полезна. Это оставляет только один реалистичный сценарий: интерфейс требует, чтобы вы определили свойства доступа к полям.

Если вы обязаны использовать свойства (через getter/setter), вы потеряете производительность прямого доступа. Для сравнения:

С интерфейсом

public class X
{
    interface ITest { int x {get; } }
    struct Test : ITest
    {
        public int x { get; set; }
    }

    public static void Main(string[] ss)
    {
        var t = new Test { x=42 };
        ITest itf = t;
    }
}

Сгенерировать вызов сеттера и бокс

.method public static  hidebysig 
       default void Main (string[] ss)  cil managed 
{
    // Method begins at RVA 0x20f4
.entrypoint
// Code size 29 (0x1d)
.maxstack 4
.locals init (
    valuetype X/Test    V_0,
    class X/ITest   V_1,
    valuetype X/Test    V_2)
IL_0000:  ldloca.s 0
IL_0002:  initobj X/Test
IL_0008:  ldloc.0 
IL_0009:  stloc.2 
IL_000a:  ldloca.s 2
IL_000c:  ldc.i4.s 0x2a
IL_000e:  call instance void valuetype X/Test::set_x(int32)
IL_0013:  ldloc.2 
IL_0014:  stloc.0 
IL_0015:  ldloc.0 
IL_0016:  box X/Test
IL_001b:  stloc.1 
IL_001c:  ret 
} // end of method X::Main

Без интерфейса

public class Y
{
    struct Test
    {
        public int x;
    }

    public static void Main(string[] ss)
    {
        var t = new Test { x=42 };
        Test copy = t;
    }
}

Создает прямое назначение и (очевидно) не бокс

// method line 2
.method public static  hidebysig 
       default void Main (string[] ss)  cil managed 
{
    // Method begins at RVA 0x20f4
.entrypoint
// Code size 24 (0x18)
.maxstack 2
.locals init (
    valuetype Y/Test    V_0,
    valuetype Y/Test    V_1,
    valuetype Y/Test    V_2)
IL_0000:  ldloca.s 0
IL_0002:  initobj Y/Test
IL_0008:  ldloc.0 
IL_0009:  stloc.2 
IL_000a:  ldloca.s 2
IL_000c:  ldc.i4.s 0x2a
IL_000e:  stfld int32 Y/Test::x
IL_0013:  ldloc.2 
IL_0014:  stloc.0 
IL_0015:  ldloc.0 
IL_0016:  stloc.1 
IL_0017:  ret 
} // end of method Y::Main

Ответ 3

Структуры и типы значений являются объектами вне иерархии объектов .net, но каждый раз, когда вы определяете структуру, система также определяет псевдокласс, полученный из ValueType, который в значительной степени ведет себя как структура; расширяющиеся операторы преобразования определяются между структурой и псевдоклассом. Обратите внимание, что переменные, параметры и поля, объявленные как тип интерфейса, всегда обрабатываются как объекты класса. Если что-то будет использоваться как интерфейс в какой-либо значительной степени, во многих случаях это может быть класс.

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

  1. Методы и свойства, которые мутируют "я" , должны быть отмечены атрибутом, который запретил бы их применение в контекстах только для чтения; методы без такого атрибута должны быть запрещены к мутации "я" , если они не скомпилированы с коммутатором совместимости.
  2. Должны быть средства передачи ссылок на структуры или их поля путем выведения определенных выражений наизнанку или с помощью стандартного типа property-delegate-pair, чтобы облегчить такие вещи, как myDictOfPoints ( "George" ). X ++;

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

    PS - Я бы предположил, что, хотя изменчивые структуры часто являются хорошей и подходящей вещью, члены структуры, которые мутируют "я" , плохо обрабатываются и их следует избегать. Либо используйте функции, которые возвратят новую структуру (например, "AfterMoving (Double distanceMiles, Double headingDegrees)" ), которая вернет новый LatLong, чья позиция была там, где он был бы после перемещения заданного расстояния), или используйте статические методы (например, "MoveDistance" (ref Позиция LatLong, Double distanceMiles, Double headingDegrees) "). Переменные структуры обычно должны использоваться в тех местах, где они по существу представляют группу переменных.

Ответ 4

Я бы использовал структуру. Класс - это излишний способ для простого использования здесь - вы можете посмотреть на другие структуры, такие как Point для примера.

Ответ 5

Вы пытаетесь создать изменчивую структуру, что нет-нет. Тем более, что он реализует интерфейс!

Если вы хотите, чтобы ваш тип LatLng изменялся, придерживайтесь ссылочного типа. В противном случае структура подходит для этого конкретного примера.

Ответ 6

Нет необходимости в интерфейсе в вашем случае. Просто сделайте его простым старым struct. Это предотвратит любой ненужный бокс при передаче struct через его интерфейс.

Ответ 7

Мне очень нравятся обоснования и рекомендации эта библиотека координат на codeplex. В ней они используют классы и представляют фактические значения для lat и долго они используют float.

Ответ 8

Лично я предпочитаю использовать классы, даже если они простые, такие как ваш LatLong. Я не использовал structs с моих дней С++. Дополнительным преимуществом классов является способность в будущем расширять их, если требуется более сложная функциональность.

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

Наконец, ваш GetHashCode кажется прославленным способом делать "Lat * Long". Я не уверен, что это безопасная ставка. Кроме того, если вы планируете многократно использовать GetHashCode в своем приложении, я бы рекомендовал сохранить его просто, чтобы улучшить производительность метода.