我应该使用结构还是类来表示Lat / Lng坐标?
我正在使用地理编码API,需要将返回点的坐标表示为纬度/经度对。 但是,我不确定是否使用一个结构或类为此。 我最初的想法是使用一个结构,但他们似乎通常在C#中皱眉(例如,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); } }
进行一些testing,我得到了以下发现:
-
一个结构总是有一个无参数的构造函数,这意味着你不能强迫它被一个构造函数实例化,这个构造函数需要两个属性(对于lat和lng),就像你可以用一个类一样。
-
结构(作为一个值types)永远不能为空,所以总是包含一个值。 但是如果实现一个接口,你仍然可以这样做:
ILatLng s = new SLatLng(); s = null;
那么在这种情况下,结构使用接口是否有意义呢?
-
如果我使用一个结构,我需要重写
Equals
,GetHashCode()
等? 我的testing表明,比较工作正常,而不这样做(不像一个类) – 这是必要的吗? -
我觉得使用课程更“舒适”,所以最好是坚持下去,因为我更了解他们的行为。 使用我的代码的人会被值types的语义混淆,特别是在处理接口时?
-
在
CLatLng
实现中,GetHashCode()
的覆盖看起来好吗? 我从这篇文章中“偷”了它,所以我不确定!
任何帮助或build议感激地收到!
说实话,我看不出有这个接口的任何一点。
我只是创build一个结构,但使其不可变 – 可变结构是一个非常糟糕的主意。 我也使用完整的Longitude
和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(); }
请注意,你需要意识到在双打上进行平等比较的正常危险 – 这里没有太多select,但是看起来应该是平等的两个值可能不是…
关于无参数构造函数的观点是合理的,但我怀疑你会发现它并不会真的咬你。
让它成为一个结构,为了性能。
- 当你处理这些结构体的数组时,性能优势会倍增很多倍。 请注意,例如,System.Collections.Generic.List正确处理.Net数组中的元素types的无箱存储,所以它也适用于通用容器。
-
请注意,您不能拥有构造函数的事实完全被C#3.5+ intializer语法所否定:
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; } }
生成setter调用和装箱
.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
结构和值types是.net对象层次结构之外的实体,但是每次定义一个结构时,系统也会定义一个从ValueType派生的伪类,其行为与结构相似; 在结构和伪类之间定义了扩展的转换运算符。 请注意,声明为接口types的variables,参数和字段始终作为类对象进行处理。 如果有什么东西会被用作任何重要的接口,在许多情况下,它可能是一个类。
虽然有人对可变结构的弊端进行了讨论,但有许多地方可变结构的价值语义将会非常有用,因为它不是系统处理它们的缺陷 。 例如:
- 变异“自我”的方法和属性应该标记一个属性,禁止它们在只读上下文中的应用; 没有这种属性的方法应该被禁止突变“自我”,除非用兼容性开关编译。
- 应该有方法通过转换某些expression式或通过具有标准types的属性委托对来传递对结构或字段的引用,以便于像myDictOfPoints(“George”)这样的事物。
通常情况下,价值types语义会比参考语义更“期待”,不仅仅是因为前者在一些通用语言中得不到很好的支持。
PS – 我会build议,虽然可变结构往往是一个很好的和适当的东西,结构成员的变异“自我”处理不好,应该避免。 或者使用返回新结构的函数(例如“AfterMoving(Double distanceMiles,Double headingDegrees)”),它将返回一个新的LatLong,其位置是移动指定距离后的位置),或者使用静态方法(例如“MoveDistance (ref LatLong position,Double distanceMiles,Double headingDegrees)“)。 可变结构通常应用于实质上代表一组variables的地方。
我会使用一个结构。 一个类是在这里简单的使用的方式矫枉过正 – 你可以看看其他的结构像Point作为例子。
你试图做一个可变的结构,这是一个禁忌。 特别是因为它实现了一个接口!
如果您希望LatLng
types可变,请使用引用types。 否则,结构对于这个特定的例子来说很好
你的情况下不需要接口。 只要使它成为一个普通的旧struct
。 这将防止任何不必要的拳击通过其接口传递struct
。
我真的很喜欢这个坐标库在codeplex上提供的理由和指导,它们使用类来表示实际值lat和long,它们使用float。
就个人而言,即使他们是像LatLong这样简单的类别,我也更喜欢使用类。 自从我的c ++时代以来我没有使用过结构。 类的额外优点是在将来需要更复杂function时扩展它们的能力。
我同意这个线程的其他人认为一个接口看起来像一个矫枉过正,因为我没有你的应用程序可能需要使用此对象的完整上下文。
最后,你的GetHashCode似乎是一个“Lat * Long”的美化方式。 我不确定这是否安全。 另外,如果你打算在你的应用程序中多次使用GetHashCode,我会build议保持简单,以提高方法的性能。