我已经对它的根结构进行了一些优化,例如如何将所有货币分母保持在一个单长。使用这段时间,我可以进行计算以“伪造”其他面额。本质上,所有东西都在内部保留为铜。
我还暗示最大基本分母值为“ 999999999”。使用这个任意数字可以使我将总货币的上限限制为999铂金,99黄金,99银色和99铜。
我只是想知道你们是否有任何建议(除了“嘿,您应该在那里完全使用var!”),这可能有助于改善当前的实现。
这是当前的代码:
using System;
namespace Some.Arbitrary.Framework
{
public enum Coins
{
/// <summary>
/// Copper is the lowest denominator of currency.
/// It requires 100 Copper to make 1 Silver.
/// </summary>
Copper = 1,
/// <summary>
/// Silver is the second most common form of currency.
/// It requires 100 Silver to Make 1 Gold.
/// </summary>
Silver = 2,
/// <summary>
/// Gold is the most common form of currency. It takes
/// part in most expensive transactions.
/// It requires 100 Gold to make 1 Platinum.
/// </summary>
Gold = 3,
/// <summary>
/// Platinum is a coin which most people never see. A single
/// Platinum coin can purchase almost anything.
/// 1 Platinum Coin = 100 Gold.
/// 1 Platinum Coin = 10,000 Silver.
/// 1 Platinum Coin = 1,000,000 Copper.
/// </summary>
Platinum = 4
}
public class MoneyBag : IEquatable<MoneyBag>, IComparable<MoneyBag>
{
private long _baseDenomination;
public const string CopperName = "Copper";
public const string SilverName = "Silver";
public const string GoldName = "Gold";
public const string PlatinumName = "Platinum";
public const char CopperAbbreviation = 'c';
public const char SilverAbbreviation = 's';
public const char GoldAbbreviation = 'g';
public const char PlatinumAbbreviation = 'p';
public const long MaximumBaseDenomination = 999999999;
public static readonly MoneyBag FilledBag = new MoneyBag(MaximumBaseDenomination);
public static readonly MoneyBag EmptyBag = new MoneyBag();
public long BaseDenomination
{
get
{
return _baseDenomination;
}
set
{
_baseDenomination = value;
// Clamp if required.
if (_baseDenomination > MaximumBaseDenomination)
{
_baseDenomination = MaximumBaseDenomination;
}
if (_baseDenomination < 0)
{
_baseDenomination = 0;
}
}
}
/// <summary>
/// The total amount of Copper.
/// </summary>
public int Copper
{
get
{
return ComputeCopper(_baseDenomination);
}
}
/// <summary>
/// The total amount of Silver.
/// </summary>
public int Silver
{
get
{
return ComputeSilver(_baseDenomination);
}
}
/// <summary>
/// The total amount of Gold.
/// </summary>
public int Gold
{
get
{
return ComputeGold(_baseDenomination);
}
}
/// <summary>
/// The total amount of Platinum.
/// </summary>
public int Platinum
{
get
{
return ComputePlatinum(_baseDenomination);
}
}
public bool IsFull
{
get
{
return _baseDenomination == MaximumBaseDenomination;
}
}
public bool IsEmpty
{
get
{
return _baseDenomination == 0;
}
}
public bool HasPlatinum
{
get
{
return Platinum > 0;
}
}
public bool HasGold
{
get
{
return Gold > 0 || Platinum > 0;
}
}
public bool HasSilver
{
get
{
return Silver > 0 || Gold > 0 || Platinum > 0;
}
}
public bool HasCopper
{
get
{
return Copper > 0 || Silver > 0 || Gold > 0 || Platinum > 0;
}
}
public MoneyBag()
{
}
public MoneyBag(int platinum, int gold, int silver, int copper)
{
Add(platinum, gold, silver, copper);
}
public MoneyBag(long baseDenomination)
{
BaseDenomination = baseDenomination;
}
public void Add(int platinum, int gold, int silver, int copper)
{
BaseDenomination += platinum * 1000000;
BaseDenomination += gold * 10000;
BaseDenomination += silver * 100;
BaseDenomination += copper;
}
public void Add(int amount, Coins type)
{
if (amount <= 0) return;
switch (type)
{
case Coins.Copper:
Add(0, 0, 0, amount);
break;
case Coins.Silver:
Add(0, 0, amount, 0);
break;
case Coins.Gold:
Add(0, amount, 0, 0);
break;
case Coins.Platinum:
Add(amount, 0, 0, 0);
break;
}
}
public void Add(MoneyBag other)
{
BaseDenomination += other._baseDenomination;
}
public void Subtract(int platinum, int gold, int silver, int copper)
{
BaseDenomination -= platinum * 1000000;
BaseDenomination -= gold * 10000;
BaseDenomination -= silver * 100;
BaseDenomination -= copper;
}
public void Subtract(int amount, Coins type)
{
if (amount <= 0) return;
switch (type)
{
case Coins.Copper:
Subtract(0, 0, 0, amount);
break;
case Coins.Silver:
Subtract(0, 0, amount, 0);
break;
case Coins.Gold:
Subtract(0, amount, 0, 0);
break;
case Coins.Platinum:
Subtract(amount, 0, 0, 0);
break;
}
}
public void Subtract(MoneyBag other)
{
BaseDenomination -= other._baseDenomination;
}
public void Empty()
{
_baseDenomination = 0;
}
public void Fill()
{
_baseDenomination = MaximumBaseDenomination;
}
public static MoneyBag operator +(MoneyBag b1, MoneyBag b2)
{
return new MoneyBag(b1._baseDenomination + b2._baseDenomination);
}
public static MoneyBag operator -(MoneyBag b1, MoneyBag b2)
{
return new MoneyBag(b1._baseDenomination - b2._baseDenomination);
}
public bool Equals(MoneyBag other)
{
return _baseDenomination == other._baseDenomination;
}
public override bool Equals(object obj)
{
return (obj is MoneyBag) && Equals((MoneyBag)obj);
}
public override int GetHashCode()
{
return _baseDenomination.GetHashCode();
}
public static bool operator ==(MoneyBag a, MoneyBag b)
{
if (ReferenceEquals(a, null)) return false;
if (ReferenceEquals(b, null)) return false;
return a.Equals(b);
}
public static bool operator !=(MoneyBag a, MoneyBag b)
{
if (ReferenceEquals(a, null)) return false;
if (ReferenceEquals(b, null)) return false;
return !a.Equals(b);
}
public static bool operator <(MoneyBag a, MoneyBag b)
{
return a.CompareTo(b) < 0;
}
public static bool operator >(MoneyBag a, MoneyBag b)
{
return a.CompareTo(b) > 0;
}
public int CompareTo(MoneyBag other)
{
// The shit was null, dumbass!
if (other == null) return 0;
if (_baseDenomination > other._baseDenomination)
{
return 1;
}
if (_baseDenomination < other._baseDenomination)
{
return -1;
}
// They were equal.
return 0;
}
public static void ComputeWealth(long baseDenomination, out int platinum, out int gold, out int silver, out int copper)
{
platinum = ComputePlatinum(baseDenomination);
gold = ComputeGold(baseDenomination);
silver = ComputeSilver(baseDenomination);
copper = ComputeCopper(baseDenomination);
}
public static int ComputeCopper(long baseDenomination)
{
return (int)Math.Floor((double)Math.Abs(baseDenomination % 100));
}
public static int ComputeSilver(long baseDenomination)
{
return (int)Math.Floor((double)Math.Abs((baseDenomination / 100) % 100));
}
public static int ComputeGold(long baseDenomination)
{
return (int)Math.Floor((double)Math.Abs((baseDenomination / 10000) % 100));
}
public static int ComputePlatinum(long baseDenomination)
{
return (int)Math.Floor((double)Math.Abs(baseDenomination / 1000000));
}
public override string ToString()
{
return
"" + Platinum + PlatinumAbbreviation + "," +
Gold + GoldAbbreviation + "," +
Silver + SilverAbbreviation + "," +
Copper + CopperAbbreviation;
}
}
}
简洁的用法示例(随意使用):
using Some.Arbitrary.Framework;
namespace SomeGameOrSomething
{
static class Program
{
static void Main()
{
MoneyBag bag = new MoneyBag(1,22,44,55);
bag.Subtract(0,50,22,0);
Console.WriteLine(bag);
Console.Read();
}
}
}
编辑/更新
如果有兴趣的人在获得所有很棒的建议后,我最终得到了什么,代码可以在这里找到:http://pastebin.com/sqVjZYry
#1 楼
对我来说,有几件事情很重要:public const string CopperName = "Copper";
public const string SilverName = "Silver";
public const string GoldName = "Gold";
public const string PlatinumName = "Platinum";
...名称和描述性文本应该(几乎)总是放在资源文件中。为什么?国际化。这意味着如果名称更改,您不必重新编译代码。
是的,您确实必须以某种方式获取查找键,但是它们实际上并不属于这里-这是功能,但是您要输出到播放器。
public long BaseDenomination
{
get
{
return _baseDenomination;
}
set
{
_baseDenomination = value;
// Clamp if required.
if (_baseDenomination > MaximumBaseDenomination)
{
_baseDenomination = MaximumBaseDenomination;
}
if (_baseDenomination < 0)
{
_baseDenomination = 0;
}
}
}
在这里使用标准的最大/最小功能可能更常见:
public long BaseDenomination
{
get
{
return _baseDenomination;
}
set
{
// Clamp if required.
_baseDenomination = Math.Max(0, Math.Min(MaximumBaseDenomination, value));
}
}
另外,使用并返回的值是
long
,但是您的最大基数恰好适合int
,您可以考虑更改它。public void Add(int platinum, int gold, int silver, int copper)
{
BaseDenomination += platinum * 1000000;
BaseDenomination += gold * 10000;
BaseDenomination += silver * 100;
BaseDenomination += copper;
}
所有这些数字?这些被称为“幻数”,而您则不需要它们。相反,您应该定义和使用常量,如下所示:
private const int SilverInCopper = 100;
private const int GoldInCopper = SilverInCopper * 100;
private const int PlatinumInCopper = GoldInCopper * 100;
您的
Add
(和Subtract
)方法有一个更为严重的缺陷,但是:您没有请注意整数溢出。他们将接受1200枚铂金币,并愉快地将钱包的内容设置为零(由于进行了钳位-尽管这假定它接近开始时的最大值)。您也不必担心正负硬币的混合,这可能很奇怪。 如果将其更改为结构没关系,但是这里没有null检查(将抛出
NullReferenceException
):public bool Equals(MoneyBag other)
{
return _baseDenomination == other._baseDenomination;
}
接下来的两个合在一起:
public static bool operator ==(MoneyBag a, MoneyBag b)
{
if (ReferenceEquals(a, null)) return false;
if (ReferenceEquals(b, null)) return false;
return a.Equals(b);
}
public static bool operator !=(MoneyBag a, MoneyBag b)
{
if (ReferenceEquals(a, null)) return false;
if (ReferenceEquals(b, null)) return false;
return !a.Equals(b);
}
具有以下效果:
((MoneyBag)null) == ((MoneyBag)null)
返回false 。((MoneyBag)null) != ((MoneyBag)null)
也返回false。同样,这是否成为结构无关紧要,但是您需要验证一个类的实例是否都不为null。一方面,这将破坏平等的交换性质。相互之间实现这些运算符也更常见:
public static bool operator !=(MoneyBag a, MoneyBag b)
{
return !(a == b);
}
比较运算符也没有null检查:
public static bool operator <(MoneyBag a, MoneyBag b)
{
return a.CompareTo(b) < 0;
}
public static bool operator >(MoneyBag a, MoneyBag b)
{
return a.CompareTo(b) > 0;
}
同样,结构没有问题,但是效果不好,否则:您将获得随机失败(
NullReferenceException
)来自其他代码,具体取决于像哪种元素排序选择枢轴。您还缺少了大于或等于和小于或等于,并且您可能想以其余四个加三个等于三个为基础(按照C / C ++约定,使用<
,小于)。 public int CompareTo(MoneyBag other)
{
// The shit was null, dumbass!
if (other == null) return 0;
if (_baseDenomination > other._baseDenomination)
{
return 1;
}
if (_baseDenomination < other._baseDenomination)
{
return -1;
}
// They were equal.
return 0;
}
作为一种样式,通常认为在您为企业编写的代码中起誓或使用其他粗俗的语言,或计划向公众发布时,这是不良的形式。
更重要的是,现在
null
被认为与所有其他元素相同。这完全打破了平等的交换性质。如果您的集合中包含null元素,则它将中断排序和搜索(如果您期望其他内容时是否为null或断言错误,将取决于它)。相反,您应该对'底部'的空值进行排序: // If other is not a valid object reference, this instance is greater.
if (other == null) return 1;
非通用
IComparable
参考也指出:根据定义,任何对象的比较结果都大于(或跟随)null,并且两个null引用的比较结果彼此相等。
(出于某种原因,该注释不在通用文档中,这是对某人的严厉监督)
此外,由于要根据内部表示进行排序,因此可以使用
long.CompareTo(...)
:public int CompareTo(MoneyBag other)
{
if (other == null) return 1;
return _baseDenomination.CompareTo(other._baseDenomination);
}
...是否使用实际基础字段还是公共成员由您自己决定。
最后,关于所有
ComputeX
方法:public static int ComputeCopper(long baseDenomination)
{
return (int)Math.Floor((double)Math.Abs(baseDenomination % 100));
}
public static int ComputePlatinum(long baseDenomination)
{
return (int)Math.Floor((double)Math.Abs(baseDenomination / 1000000));
}
...您在其中所做的所有数学运算都是整数运算。如果您不知道这是什么,我建议您仔细阅读,但要点是:
int i1 = 10;
int i2 = 3;
Console.Out.WriteLine(10 / 3); // Prints '3'
本质上,语言(几乎所有计算机语言都以这种方式工作)正在截断结果(对于正数,这等于下限)。
它们还将负数变成正数!这可能在代码的其他地方可以利用-钳位到
0
或返回负值。哦,
ComputePlatinum
也遇到整数溢出:输入是long
,但是输出是int
。足够大的正值会变成...别的,很有可能是负数。您应该在这里返回long
,或者首先只获取int
。 (或者使用checked
并引发异常,但这可能有问题)在任何情况下,我都可能按照以下方式编写方法:public static int ComputeCopper(int baseDenomination)
{
return baseDenomination % SilverInCopper;
}
public static int ComputeSilver(int baseDenomination)
{
return baseDenomination % GoldInCopper / SilverInCopper ;
}
// I assume you can figure out ComputeGold
public static int ComputePlatinum(int baseDenomination)
{
return baseDenomination / PlatinumInCopper;
}
评论
\ $ \ begingroup \ $
非常感谢您指出加减法中的疏漏。我将立即修复它。
\ $ \ endgroup \ $
– Krythic
16 Mar 16 '16 at 10:57
\ $ \ begingroup \ $
我也只想说,您的所有建议都很棒,并且经过深思熟虑。
\ $ \ endgroup \ $
– Krythic
16 Mar 16 '16 at 11:03
\ $ \ begingroup \ $
“国际化”也许意味着“内部化”? AFAIK国际化仅用于使某些内容在所有地区/平台之间都兼容。
\ $ \ endgroup \ $
– TylerH
16 Mar 17 '16 at 16:49
\ $ \ begingroup \ $
@TylerH可以肯定,这是自动更正的结果。不过,他的观点仍然明确。
\ $ \ endgroup \ $
– Krythic
16 Mar 17 '16 at 17:39
\ $ \ begingroup \ $
@TylerH-不,我的意思是国际化。但是,即使您不打算将其分布到多个区域,通常也最好将字符串从程序员那里拿走。我们常常不知道该写些什么(这是另一门学科)。它使您可以将工作转移给非程序员。即使对于单个“区域”,通常也有特定的法律要求。多个国家/地区对双语(或更多语言)提出了要求-在加拿大,如果您在魁北克经商,则必须同时包括法语和英语。首先假设事物将被更广泛地使用。
\ $ \ endgroup \ $
–钟表缪斯
16 Mar 17 '16 at 22:01
#2 楼
public enum Coins
枚举类型名称应为单数,除非用
[Flags]
属性修饰:对Enum类型和值名称使用Pascal大小写。
尽量少用缩写。
不要对Enum类型名称使用Enum后缀。
对于大多数Enum类型,请使用单数名称。属于位字段的枚举类型的名称。
始终将FlagsAttribute添加到位字段枚举类型。
https://msdn.microsoft.com/zh-cn/library/4x252001( v = vs.71).aspx
因此,
CoinType
是一个更好的名称。现在,由于这不是[Flags]
枚举,因此基本的int
值完全没有意义,不需要完全指定。public const char CopperAbbreviation = 'c';
public const char SilverAbbreviation = 's';
public const char GoldAbbreviation = 'g';
public const char PlatinumAbbreviation = 'p';
从概念上讲,这些不是常数。它们是您在此版本的代码中现在要使用的值。问题是,通过将它们公开为
public const
字段,如果发布了库,您几乎已经陷入了困境:如果将来的版本更改了这些值,则您将强制重新编译所有客户端代码,因为const
值在编译时被“烧入到位”-因此,如果我正在使用您的库,而现在您要发布新版本,则必须重新编译代码以获取框架提供的新值。另一方面,如果您有这样的产品:
public static readonly char CopperAbbreviation = 'c';
public static readonly char SilverAbbreviation = 's';
public static readonly char GoldAbbreviation = 'g';
public static readonly char PlatinumAbbreviation = 'p';
..那么我可以将旧版本换成新版本,更新的内容而无需重新编译任何内容。
这就是说,这些常量的唯一用途似乎是在
ToString
实现中,该实现将所有值连接在一起:public override string ToString()
{
return
"" + Platinum + PlatinumAbbreviation + "," +
Gold + GoldAbbreviation + "," +
Silver + SilverAbbreviation + "," +
Copper + CopperAbbreviation;
}
这些串联不是很漂亮,而
""
就像是一种破解方法,可以让+
运算符进行编译并隐式获取数值转换为字符串。如果该
ToString
替代用于调试目的,那么我什至不会理会XxxxAbbreviation
的值-让客户端代码弄清楚它,如果您确实想要ToString
替代,那么您可以简单地做到这一点:return string.Format("{0}p, {1}g, {2}s, {3}c", Platinum, Gold, Silver, Copper);
更好的是,用
DebuggerDisplayAttribute
装饰班级:[DebuggerDisplay("{Platinum}p, {Gold}g, {Silver}s, {Copper}c")]
public static void ComputeWealth(long baseDenomination, out int platinum, out int gold, out int silver, out int copper)
这些
out
参数很臭。不幸的是,该方法返回void
,而您只需用一条小指令就可以简单地返回MoneyBag
实例:return new MoneyBag(baseDenomination);
但是,那为什么还要用那个成员?删除它是多余的。
考虑一下,整个类型实际上封装了4个
int
值:请考虑使其成为不可变的struct
而不是类;所有的变异方法都将简单地返回一个新值。我只更改这两个成员:
public static readonly MoneyBag FilledBag = new MoneyBag(MaximumBaseDenomination);
public static readonly MoneyBag EmptyBag = new MoneyBag();
public static readonly MoneyBag Full = new MoneyBag(MaximumBaseDenomination);
public static readonly MoneyBag Empty = new MoneyBag();
调用时,“包”是多余的。考虑:
var emptyBag = MoneyBag.EmptyBag;
var fullBag = MoneyBag.FilledBag;
vs.
var emptyBag = MoneyBag.Empty;
var fullBag = MoneyBag.Full;
评论
\ $ \ begingroup \ $
快要结束时,我开始同意你的看法,但其他一切都在挑剔。另外,此代码仅在我的游戏中使用;它不会被分发,因此我将保留const char字段。
\ $ \ endgroup \ $
– Krythic
16 Mar 16 '16 at 2:19
\ $ \ begingroup \ $
是的,ToString开头的“”是一个肮脏的东西。 =)
\ $ \ endgroup \ $
– Krythic
16 Mar 16 '16 at 2:20
\ $ \ begingroup \ $
该类实际上应该是一个结构。仅仅是因为您对GetHashCode使用了可变字段。如果坚持最佳实践是挑剔的话,那么我就是一个快乐而自豪的挑剔者。
\ $ \ endgroup \ $
–马修·金登(Mathieu Guindon)♦
16 Mar 16 '16 at 2:23
\ $ \ begingroup \ $
我将使它成为一个结构;在发布之前,我已经考虑过这一点。
\ $ \ endgroup \ $
– Krythic
16 Mar 16 '16 at 2:28
\ $ \ begingroup \ $
@Krythic-如果确实将其设为结构,则它应该是不可变的。出于优化目的,允许CLR使用小型结构来做一些有趣的事情,对它们进行突变并不总是可以像某些人所期望的那样工作。
\ $ \ endgroup \ $
–钟表缪斯
16 Mar 16 '16 at 8:32
#3 楼
您确定您的业务逻辑确实需要了解所有这些不同类型的硬币吗?对我来说,这感觉像是不必要的麻烦。维护代码总是容易得多,该代码始终将钱简单地视为long
数字(带有一些其他方法)。仅在涉及UI层时,才应考虑什么是显示钱袋的最佳方法。在那里,您可以拥有一个将货币转换为硬币的转换器,您的用户会在屏幕上看到该转换器。但是它应该是一个与MoneyBag
分开的类,并且仅应作为UI层的一部分存在。关于您现有代码的其他一些信息:
实现相等方法和运算符时,应始终尝试尽可能多地重用实现。
a != b
与!(a == b)
相同,a - b
与a + (-b)
等,例如,您可以将相等实现为:public override bool Equals(object obj)
{
return Equals(obj as MoneyBag);
}
public static bool operator ==(MoneyBag a, MoneyBag b)
{
return a.Equals(b);
}
public static bool operator !=(MoneyBag a, MoneyBag b)
{
return !a.Equals(b); // or return !(a == b)
}
public bool Equals(MoneyBag other)
{
//here goes the actual implementation,
//which you reuse in all other methods
}
这种方式可以保证所有相等方法总是返回相同的结果。而且,修复相等逻辑中的错误就像修复单个方法一样容易。例如,目前将
null
传递给IEquatable.Equals
方法将实现throw
,而使用==
则可以正常工作。例如,您具有可变的哈希码,这通常是个坏主意。
您具有可变的静态字段(
FilledBag
和EmptyBag
),这也不是一个好主意。考虑以下代码:var myBag = MoneyBag.EmptyBag;
//blah-blah 100 lines of code
myBag.Add(...);
您可能会想:“嘿,我不是那个家伙!我不会那样使用
EmptyBag
!”但是,每个THOSE家伙都可能有相同的想法。platinum * 1000000
之类的东西应该提取到专用方法中,因此您不必每次都复制粘贴(或手动计数零)需要将一种货币转换为另一种货币。评论
\ $ \ begingroup \ $
您对相等性的建议还会由于无限递归而导致堆栈溢出。
\ $ \ endgroup \ $
– Krythic
16 Mar 16 '16 at 10:34
\ $ \ begingroup \ $
@Krythic,不,我了解得很好。您确实在内部使用了很长的时间(这就是为什么我建议这样做,您可能应该使用uint代替)。但是,您还使用CoinTypes,具有诸如Silver的属性,并且在MoneyBag上具有诸如AddCoins的方法。我要问的问题是,当您只需要一个货币时,为什么要在您的业务层中处理所有这些不同的货币和兑换?不,对等方法没有递归。但是您显然不愿意进行讽刺,所以我将以此为生。祝你今天愉快。
\ $ \ endgroup \ $
– Nikita B
16-3-16在10:38
\ $ \ begingroup \ $
@Krythic答案的最重要的一点是:为什么还要在代码中烦恼所有这些?您应该在任何地方都只使用一个整数表示金钱。在用户界面中,将有一个方法“ displayMoneyParts”,它将为玩家呈现钱款。
\ $ \ endgroup \ $
– Falco
16 Mar 16 '16 at 12:15
\ $ \ begingroup \ $
@Krythic如果您只有一个货币格式化程序类,则不会。这样,您可以重用逻辑或轻松扩展它。这将是DRY,遵循SRP,并遵守开放/关闭原则。
\ $ \ endgroup \ $
– Erik
16 Mar 16 '16 at 18:58
\ $ \ begingroup \ $
@Krythic提出他的观点的另一种方法(您似乎误解了)是,存储和修改货币的业务逻辑与显示货币的逻辑应在不同的类别中。您可能想要这样做的原因之一:对于不同的语言,拥有CurrencyConverter的多个扩展(例如)将变得微不足道。也许用一种语言,您只需要3种形式的货币。无论哪种方式,用于修改用户资金的业务逻辑都不必处理货币的转换-取决于另一类。
\ $ \ endgroup \ $
–赞恩
16 Mar 19 '16 at 17:48
#4 楼
已经有很多针对代码样式和代码转换的好答案,所以我想换个角度:作为代码的潜在用户/调试器。加/减不是反向操作
固守价值观对它似乎可以为我们提供安全保障具有吸引力,但同时也危及“最少惊喜原则”。如果我有500p,再加上800p,我最终会得到999p(和99g99s99c)。如果我再减去800p,则没有原来的500p;反之,相反,我只剩下不到200p的硬币。
如果您担心如何在有限的空间内显示大量资金,则可以考虑使用该组件。如果需要更多空间,它可能会逐渐省去铜/银/金币。如果我有那么多钱,我就不用担心我有多少铜币。 ;)
对失败没有反馈
(我不是C#程序员,所以我可能有这个错误。)
看来我可以从20个硬币中减去50个硬币,最终得到0个硬币。这让我感到惊讶。如果某个函数无法执行其声称要做的(或似乎声称要做的—棘手的),则应以某种方式发出错误信号,例如引发异常。
HasXYZ vs GetXYZ
您有200cp或2sp的地方,这里也是一样,HasCopper()将返回
true
,因为它还会检查HasSilver(),但是Copper()将返回0。在我看来,HasCopper()应该暗示Copper()> 0 ,因此HasCopper()应该返回false
或Copper()应该给出铜币的总价值(200)。简化或变大
我的基本建议是:
通过消除硬币类型并将其留在表示中,使事情变得更简单;
或一路走来并分别计算所有不同的硬币类型,即为每种类型都有一个计数器,然后添加一个Wealth()或Value()函数,通过添加所有硬币值可为您提供总价值。加法会更容易,但减法会更困难。
毕竟,如果这种区别没有意义,那么遇到4种不同硬币类型的麻烦有什么意义呢? (考虑一下如何使用该类:您将以铂金,金,银,铜币为商店中的商品设置价格吗?还是将基础设置为“价值”?)
评论
\ $ \ begingroup \ $
“ HasXYZ vs GetXYZ”:这个新实现如何呢?pastebin.com/b9mGqXdm?
\ $ \ endgroup \ $
– Krythic
16-3-17的1:34
\ $ \ begingroup \ $
“或者您会设定一个“价值”基础?”我将计算铜的基本价格,然后进行比较以查看玩家是否有足够的钱,然后如果有的话,只需从当前的钱中减去该金额即可。
\ $ \ endgroup \ $
– Krythic
16-3-17的1:44
\ $ \ begingroup \ $
“加/减不是逆运算”首先让我说,在我的游戏中实际达到999个白金可能永远不会发生。从概念上讲,铂金永远不会从敌人的遭遇中掉落,并且1或2金币也将是罕见的情况。铂金的上限(太大)几乎是任何人都不现实的。我早些时候设置了一个测试用例,每隔几秒钟就给我1枚银牌,直到花了白金才花了很长时间。唯一的另一件事就是下界,我对此感到满意,因为它不能消极。
\ $ \ endgroup \ $
– Krythic
16-3-17的1:48
\ $ \ begingroup \ $
@Krythic加/减逆:不消极是一件好事。不会在没有引发异常的情况下达到零。上限也是如此:如果您需要一个限制,请强制执行它,但是请考虑告知您的班级用户事情没有按计划进行。既然您永远都不会达到这个极限,那么异常不会伤害任何人,对吗? :)
\ $ \ endgroup \ $
– JvR
16 Mar 17 '16 at 12:39
\ $ \ begingroup \ $
其他地方的逻辑将处理该问题,例如将钱提取/存入游戏中的银行。如果银行已满,我将拒绝该操作。
\ $ \ endgroup \ $
– Krythic
16 Mar 17 '16 at 13:11
#5 楼
看来您已经完全混淆了业务和显示层/职责。您可以通过消除大多数功能中的c / s / g / p分隔来简化事情。例如,Add(int amount, Coins type)
和Add(int platinum, int gold, int silver, int copper)
不需要存在。如果您只在显示器之外的任何地方都坚持基本价值,那么您将减少以下工作:存储(这只是一个价值)
购物(同样,一个价值,而不是四个)
经济修复(您要重新计算所有值,还是在将来某个时候做
prices*=1.1
)任何会影响价格的部分功能(讨价还价?)
HasCopper
和类似方法不是超级直观。 可变的空/装满的袋子是等待发生的事故。
我不明白为什么您使用
1 gold
在HasCopper
函数中。他们不应该永远是负面的,对吗?#6 楼
使用单个数字类型。金/银/等只是您模型的一种视图,这将大大简化所有操作。所有价格实际上都在您的基本数字类型中,但是以友好的方式查看,并且所有数学运算仅在您的数字类型上执行。#7 楼
我想知道您为什么要完全分裂价值观,甚至更奇怪为什么还没有指出。当您查看您的银行帐户时,您不会说我有10 $ 100加上50 $ 10您只是说我有$ 1500。拆分您的货币只是引入了很多潜在的错误,而且对于前端。我只是让货币数量成为一个数字,然后让前端处理将其分为铜,银,金。我认为,显示货币的方式应该与您的游戏逻辑无关。
评论
\ $ \ begingroup \ $
这是设计决定。
\ $ \ endgroup \ $
– Krythic
16 Mar 17 '16 at 13:09
\ $ \ begingroup \ $
很抱歉,但是我看不到为什么您会在游戏逻辑中拆分黄金的价值,而又不将实际的硬币用于重量之类的事情。由于缺少硬舍入和使用基值,您似乎不愿意这样做。因此,为什么不仅仅使用和存储您的基本值并让您的前端进行分类,用户将获得的所有体验就是,他或她可能会看到硬币的图像,而在正常货币中,点或逗号将是硬币。 (1,000,000变成(g)1(s)000(c)000)我认为您可以通过在前端解决这个问题来简化您的问题。
\ $ \ endgroup \ $
–尼尔斯
16-3-18在8:21
评论
计算HasFoo时,只需重用高阶函数。 HasGold =黄金> 0 ||例如,HasPlatinum。我为您进行了优化:999999999小于int.MaxValue,因此您可以将其全部存储为int而不是long。您也可以使用uint,因为您不允许使用负数。
旁注-在大多数RPG中,钱实际上只存储在铜中。仅出于显示目的,它分为金/银/板。另外,您让我想启动Win3.11 VM并立即播放《风之城堡》 ...
我为您做了大量优化:1024 * 128 * 128 * 128-1很小,可以容纳一个有符号的int,因此您可以轻松地放入999 * 128 * 128 * 128。这意味着您可以使用位移而不是乘以10的幂,仍然可以容纳同样多的值。
@ wizzwizz4甚至没有关闭。左移或右移仅比mod快3倍。 (自己运行... pastebin.com/rNEztD3h)考虑到RPG中发生的所有其他情况,这不是“大规模”的改进。这并不是代码的关键组成部分。如果花费超过服务器时间0.01%的钱进行计算,则某处存在严重问题。