我正在使用地理编码API,需要将返回点的坐标表示为纬度/经度对。但是,我不确定是否为此使用结构或类。我最初的想法是使用一个结构,但它们似乎在C#中通常不受欢迎(例如,Jon Skeet提到in this answer,“我几乎从未定义自定义结构”)。性能和内存使用不是应用程序中的关键因素。
到目前为止,我已经基于一个简单的接口提出了这两个实现:
接口
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 Struct Implementation
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);
}
}
进行一些测试我得出以下结论:
一个struct总是有一个无参数构造函数,这意味着你不能强制它用一个构造函数来实例化,这个构造函数需要两个属性(对于lat和lng),就像你可以用一个类一样。
结构(作为值类型)永远不能为空,因此将始终包含值。但是如果实现一个接口,你仍然可以做这样的事情:
ILatLng s = new SLatLng(); s = null;
在这种情况下,结构使用接口是否有意义?
如果我使用结构,我是否需要覆盖Equals
,GetHashCode()
等?我的测试表明比较工作没有这样做(与课程不同) - 所以有必要吗?
我觉得使用课程更“舒服”,所以最好坚持使用它们,因为我更了解它们的行为方式?使用我的代码的人是否会被值类型语义所困惑,特别是在使用界面时?
在CLatLng
实施中,GetHashCode()
的覆盖是否正常?我从这篇文章中偷了它,所以我不确定!
感激不尽的任何帮助或建议!
答案 0 :(得分:56)
说实话,我没有任何关于为此设置界面的观点。
我只是创建一个结构,但让它变成不可变的 - 可变结构是一个非常糟糕的主意。我还使用完整的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();
}
请注意,您需要了解对双打执行相等比较的正常危险 - 此处没有太多替代方案,但看起来的两个值应该相同,可能不是......
关于无参数构造函数的观点是合理的,但我怀疑你会发现它不会实际上咬你。
答案 1 :(得分:9)
使其成为一个结构,以提高性能。
请注意,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
答案 2 :(得分:4)
结构和值类型是.net对象层次结构之外的实体,但每次定义结构时,系统还定义了一个从ValueType派生的伪类,其主要行为类似于struct;在struct和pseudo-class之间定义了扩展转换运算符。请注意,声明为接口类型的变量,参数和字段始终作为类对象处理。如果某些东西将被用作任何重要程度的界面,在许多情况下它也可能是一个类。
尽管有些人讨论了可变结构的邪恶,但是有很多地方可以使用具有值语义的可变结构非常有用如果它不是系统处理它们的缺陷。例如:
通常情况下,价值类型的语义学比参考语义更“预期”,不仅仅是因为前者在某些常见语言中得到了很好的支持。
PS - 我认为虽然可变结构通常是一个好的和适当的东西,但是变异“自我”的结构成员处理得很糟糕,应该避免。使用将返回新结构的函数(例如“AfterMoving(Double distanceMiles,Double headingDegrees)”)将返回一个新的LatLong,其位置是移动指定距离后的位置),或者使用静态方法(例如“MoveDistance”) (参考LatLong位置,Double distanceMiles,Double headingDegrees)“)。可变结构通常应该用在它们基本上代表一组变量的地方。答案 3 :(得分:3)
我会使用结构。对于这里的简单使用来说,类是过度杀戮 - 你可以看看像Point这样的其他结构。
答案 4 :(得分:3)
你正试图制作一个可变的结构,这是一个禁忌。特别是因为它实现了一个接口!
如果您希望LatLng
类型可变,请坚持使用引用类型。否则struct对于这个特定的例子是好的。
答案 5 :(得分:2)
在您的情况下不需要接口。只需使它成为一个普通的struct
。这将通过其界面传递struct
时防止任何不必要的装箱。
答案 6 :(得分:1)
我非常喜欢this coordinate library on codeplex提供的理由和指导。在其中,他们使用类来表示lat和long使用float的实际值。
答案 7 :(得分:0)
就个人而言,我更喜欢使用课程,即使它们是像LatLong这样简单的课程。我从c ++时代开始就没有使用过结构体。如果需要更复杂的功能,类的额外优势是将来能够扩展它们。
我同意这个帖子中的其他人认为接口看起来有点过分,因为我没有关于你使用这个对象的完整上下文,因为它可能在你的应用程序中需要。
最后,你的GetHashCode似乎是做“Lat * Long”的美化方式。我不确定这是不是一个安全的赌注。此外,如果您计划在应用程序中多次使用GetHashCode,我建议您保持简单,以提高方法的性能。