Должен ли я использовать структуру или класс для представления координат Lat / Lng? - PullRequest
37 голосов
/ 27 мая 2011

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

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

Интерфейс

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;

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

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

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

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

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

Ответы [ 8 ]

56 голосов
/ 27 мая 2011

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

Я бы просто создал структуру, но сделал ее неизменной - изменяемые структуры - действительно плохая идея.Я бы также использовал полные 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();
}

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

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

9 голосов
/ 27 мая 2011

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

  • Увеличение производительности многократно возрастет, например, при обработке, например, массивы этих структур. Обратите внимание, что, например, System.Collections.Generic.List правильно обрабатывает неупакованное хранилище типа элемента в массивах .Net, поэтому оно также применимо к универсальным контейнерам.
  • Обратите внимание, что тот факт, что у вас не может быть конструктора, полностью отрицается синтаксисом инициализатора C # 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
4 голосов
/ 16 июня 2011

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

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

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

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

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

3 голосов
/ 27 мая 2011

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

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

3 голосов
/ 27 мая 2011

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

2 голосов
/ 27 мая 2011

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

1 голос
/ 19 марта 2015

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

0 голосов
/ 27 мая 2011

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

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

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

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