在 child class 中获得正确实现的重载的正确方法
Proper way to get the right implemented overload in a child class
先介绍一下我在做什么
在 Unity 中,我想让一些 GameObject
s(比如玩家)能够拾取物品(其他 GameObject
s)。
为了做到这一点,我设计了这个基本代码:
拉取物品的组件:
public class PickupMagnet : MonoBehaviour
{
// [...]
private void Update()
{
Transform item = FindClosestItemInRange(); // Well, this line doesn't exist, it's just a simplification.
if (ítem != null)
Pickup(item);
}
private void Pickup(Transform item)
{
IPickup pickup = item.GetComponent<IPickup>();
if (pickup != null)
{
pickup.Pickup();
Destroy(item);
}
}
}
那些(目前)项目的界面:
public interface IPickup
{
void Pickup();
// [...]
}
以及我当时做的单品:
public class Coin : MonoBehaviour, IPickup
{
private int price;
// [...]
void IPickup.Pickup()
{
Global.money += price; // Increase player money
}
// [...]
}
一切正常,直到我想添加一个新项目:健康包。这个项目,增加捡起它的生物的生命值。但为了做到这一点,我需要生物脚本的实例:LivingObject
.
public class HealthPack: MonoBehaviour, IPickup
{
private int healthRestored;
// [...]
void IPickup.Pickup(LivingObject livingObject)
{
livingObject.TakeHealing(healthRestored);
}
// [...]
}
问题是 IPickup.Pickup()
没有任何参数。显然,我可以将其更改为 IPickup.Pickup(LivingObject livingObject)
并忽略 Coin.Pickup
上的参数,但如果将来我想添加更多种类的项目,需要不同的参数怎么办?
其他选择是向接口添加一个新方法,但这迫使我实现 Coin.Pickup(LivingObject livingObject)
并实现它。
想了想把IPickup去掉,换成:
public abstract class Pickupable : MonoBehaviour
{
// [...]
public abstract bool ShouldBeDestroyedOnPickup { get; }
public virtual void Pickup() => throw new NotImplementedException();
public virtual void Pickup(LivingObject livingObject) => throw new NotImplementedException();
}
然后覆盖 Coin
和 HealthPack
中的必要方法。另外,我将 PickupMagnet.Pickup(Transform item)
更改为:
public class PickupMagnet : MonoBehaviour
{
// [...]
private LivingObject livingObject;
private void Start()
{
livingObject = gameObject.GetComponent<LivingObject>();
}
// [...]
private void Pickup(Transform item)
{
Pickupable pickup = item.GetComponent<Pickupable>();
if (pickup != null)
{
Action[] actions = new Action[] { pickup.Pickup, () => pickup.Pickup(livingObject) };
bool hasFoundImplementedMethod = false;
foreach (Action action in actions)
{
try
{
action();
hasFoundImplementedMethod = true;
break;
}
catch (NotImplementedException) { }
}
if (!hasFoundImplementedMethod)
throw new NotImplementedException($"The {item.gameObject}'s {nameof(Pickup)} class lack of any Pickup method implementation.");
else if (pickup.ShouldBeDestroyedOnPickup)
Destroy(item.gameObject);
}
}
}
基本上,这会遍历 actions
中定义的所有方法并执行它们。如果他们提出 NotImplementedException
它会继续尝试数组中的其他方法。
此代码运行良好,但就我个人而言,我不喜欢在 Pickable.Pickup
.
的每次重载中定义该数组的想法
所以,我开始做一些研究,我发现了一个叫做 "reflection" 的东西。我仍然不确定它是如何工作的,但我设法编写了这个工作代码。
private void Pickup(Transform item)
{
Pickupable pickup = item.GetComponent<Pickupable>();
if (pickup != null)
{
bool hasFoundImplementedMethod = false;
foreach (MethodInfo method in typeof(Pickupable).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
if (method.Name == "Pickup")
{
ParameterInfo[] parametersGetted = method.GetParameters();
int parametersAmount = parametersGetted.Length;
object[] parametersObjects = new object[parametersAmount ];
for (int i = 0; i < parametersAmount; i++)
{
Type parameterType = parametersGetted[i].ParameterType;
if (parameters.TryGetValue(parameterType, out object parameterObject))
parametersObjects[i] = parameterObject;
else
throw new KeyNotFoundException($"The key Type {parameterType} was not found in the {nameof(parameters)} dictionary.");
}
bool succed = TryCatchInvoke(pickup, method, parametersObjects);
if (succed) hasFoundImplementedMethod = true;
}
}
if (!hasFoundImplementedMethod)
throw new NotImplementedException($"The {item.gameObject}'s {nameof(Pickup)} class lack of any Pickup method implementation.");
else if (pickup.ShouldBeDestroyedOnPickup)
Destroy(item.gameObject);
}
}
private bool TryCatchInvoke(Pickupable instance, MethodInfo method, object[] args)
{
try
{
method.Invoke(instance, args);
return true;
}
catch (Exception) // NotImplementedException doesn't work...
{
return false;
}
}
并添加到 MagnetPickup
:
private LivingObject livingObject;
private Dictionary<Type, object> parameters;
private void Start()
{
livingObject = gameObject.GetComponent<LivingObject>();
parameters = new Dictionary<Type, object> { { typeof(LivingObject), livingObject } };
}
...并且有效。
我对 Unity profiler 不是很熟悉,但我认为最后的代码运行速度有点快(不到 1%)。
问题是我不确定该代码将来是否会给我带来问题,所以这是我的问题:反射是解决这个问题的正确方法还是我应该使用我的try/catch 尝试或其他代码?
对于仅仅 1% 我不确定我是否应该冒险使用它。我不是在寻找最好的性能,只是在寻找解决这个问题的正确方法。
我确实认为这样做的最佳方法是在 Pickup 中发送对 Player 对象的引用,然后针对每种类型的 Pickup 对象执行自定义逻辑。我可能会跳过界面,只拥有一个名为 "PickupObject" 或其他名称的基础对象,然后让 FindClosestItemInRange return those.
最后,您应该销毁 GameObject,而不是您传递给 Pickup 函数的任何东西。您可能可以销毁 Transform 并获得相同的结果(我没有尝试过),但实际上销毁 GameObject 而不是 GameObject 的任何组件只是一个好习惯
public class PickupObject : MonoBehaviour
{
virtual void Pickup(Player playerObject) { }
}
public class Coin : PickupObject
{
public int price;
override void Pickup(Player playerObject)
{
playerObject.money += price; // Move money over to the player as it probably makes more sense
}
}
public class HealthPack : PickupObject
{
public int healthRestored;
override void Pickup(Player playerObject)
{
playerObject.health += healthRestored;
}
}
public class PickupMagnet : MonoBehaviour
{
public Player PlayerObject;
private void Update()
{
PickupObject item = FindClosestItemInRange();
Pickup(item);
}
private void Pickup(PickupObject pickup)
{
pickup.Pickup(PlayerObject);
Destroy(pickup.gameObject);
}
}
编辑,关于您的代码的一些一般想法:
如果您的通用 "LivingObject" 可以像您的代码建议的那样同时获取生命值和金币,那么您可能过于通用了。听起来你只是有一个需要拾取东西的玩家。让 "anything" 能够拾取 "anything" 以我的经验来说太笼统了。不要试图在第一行代码中解决所有未来的问题。如果你不确定你要去哪里,或者在这个早期阶段结构很棘手。编写执行您需要它执行的操作的代码,并在出现模式和重复时重构它。
先介绍一下我在做什么
在 Unity 中,我想让一些 GameObject
s(比如玩家)能够拾取物品(其他 GameObject
s)。
为了做到这一点,我设计了这个基本代码:
拉取物品的组件:
public class PickupMagnet : MonoBehaviour
{
// [...]
private void Update()
{
Transform item = FindClosestItemInRange(); // Well, this line doesn't exist, it's just a simplification.
if (ítem != null)
Pickup(item);
}
private void Pickup(Transform item)
{
IPickup pickup = item.GetComponent<IPickup>();
if (pickup != null)
{
pickup.Pickup();
Destroy(item);
}
}
}
那些(目前)项目的界面:
public interface IPickup
{
void Pickup();
// [...]
}
以及我当时做的单品:
public class Coin : MonoBehaviour, IPickup
{
private int price;
// [...]
void IPickup.Pickup()
{
Global.money += price; // Increase player money
}
// [...]
}
一切正常,直到我想添加一个新项目:健康包。这个项目,增加捡起它的生物的生命值。但为了做到这一点,我需要生物脚本的实例:LivingObject
.
public class HealthPack: MonoBehaviour, IPickup
{
private int healthRestored;
// [...]
void IPickup.Pickup(LivingObject livingObject)
{
livingObject.TakeHealing(healthRestored);
}
// [...]
}
问题是 IPickup.Pickup()
没有任何参数。显然,我可以将其更改为 IPickup.Pickup(LivingObject livingObject)
并忽略 Coin.Pickup
上的参数,但如果将来我想添加更多种类的项目,需要不同的参数怎么办?
其他选择是向接口添加一个新方法,但这迫使我实现 Coin.Pickup(LivingObject livingObject)
并实现它。
想了想把IPickup去掉,换成:
public abstract class Pickupable : MonoBehaviour
{
// [...]
public abstract bool ShouldBeDestroyedOnPickup { get; }
public virtual void Pickup() => throw new NotImplementedException();
public virtual void Pickup(LivingObject livingObject) => throw new NotImplementedException();
}
然后覆盖 Coin
和 HealthPack
中的必要方法。另外,我将 PickupMagnet.Pickup(Transform item)
更改为:
public class PickupMagnet : MonoBehaviour
{
// [...]
private LivingObject livingObject;
private void Start()
{
livingObject = gameObject.GetComponent<LivingObject>();
}
// [...]
private void Pickup(Transform item)
{
Pickupable pickup = item.GetComponent<Pickupable>();
if (pickup != null)
{
Action[] actions = new Action[] { pickup.Pickup, () => pickup.Pickup(livingObject) };
bool hasFoundImplementedMethod = false;
foreach (Action action in actions)
{
try
{
action();
hasFoundImplementedMethod = true;
break;
}
catch (NotImplementedException) { }
}
if (!hasFoundImplementedMethod)
throw new NotImplementedException($"The {item.gameObject}'s {nameof(Pickup)} class lack of any Pickup method implementation.");
else if (pickup.ShouldBeDestroyedOnPickup)
Destroy(item.gameObject);
}
}
}
基本上,这会遍历 actions
中定义的所有方法并执行它们。如果他们提出 NotImplementedException
它会继续尝试数组中的其他方法。
此代码运行良好,但就我个人而言,我不喜欢在 Pickable.Pickup
.
所以,我开始做一些研究,我发现了一个叫做 "reflection" 的东西。我仍然不确定它是如何工作的,但我设法编写了这个工作代码。
private void Pickup(Transform item)
{
Pickupable pickup = item.GetComponent<Pickupable>();
if (pickup != null)
{
bool hasFoundImplementedMethod = false;
foreach (MethodInfo method in typeof(Pickupable).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
if (method.Name == "Pickup")
{
ParameterInfo[] parametersGetted = method.GetParameters();
int parametersAmount = parametersGetted.Length;
object[] parametersObjects = new object[parametersAmount ];
for (int i = 0; i < parametersAmount; i++)
{
Type parameterType = parametersGetted[i].ParameterType;
if (parameters.TryGetValue(parameterType, out object parameterObject))
parametersObjects[i] = parameterObject;
else
throw new KeyNotFoundException($"The key Type {parameterType} was not found in the {nameof(parameters)} dictionary.");
}
bool succed = TryCatchInvoke(pickup, method, parametersObjects);
if (succed) hasFoundImplementedMethod = true;
}
}
if (!hasFoundImplementedMethod)
throw new NotImplementedException($"The {item.gameObject}'s {nameof(Pickup)} class lack of any Pickup method implementation.");
else if (pickup.ShouldBeDestroyedOnPickup)
Destroy(item.gameObject);
}
}
private bool TryCatchInvoke(Pickupable instance, MethodInfo method, object[] args)
{
try
{
method.Invoke(instance, args);
return true;
}
catch (Exception) // NotImplementedException doesn't work...
{
return false;
}
}
并添加到 MagnetPickup
:
private LivingObject livingObject;
private Dictionary<Type, object> parameters;
private void Start()
{
livingObject = gameObject.GetComponent<LivingObject>();
parameters = new Dictionary<Type, object> { { typeof(LivingObject), livingObject } };
}
...并且有效。
我对 Unity profiler 不是很熟悉,但我认为最后的代码运行速度有点快(不到 1%)。
问题是我不确定该代码将来是否会给我带来问题,所以这是我的问题:反射是解决这个问题的正确方法还是我应该使用我的try/catch 尝试或其他代码?
对于仅仅 1% 我不确定我是否应该冒险使用它。我不是在寻找最好的性能,只是在寻找解决这个问题的正确方法。
我确实认为这样做的最佳方法是在 Pickup 中发送对 Player 对象的引用,然后针对每种类型的 Pickup 对象执行自定义逻辑。我可能会跳过界面,只拥有一个名为 "PickupObject" 或其他名称的基础对象,然后让 FindClosestItemInRange return those.
最后,您应该销毁 GameObject,而不是您传递给 Pickup 函数的任何东西。您可能可以销毁 Transform 并获得相同的结果(我没有尝试过),但实际上销毁 GameObject 而不是 GameObject 的任何组件只是一个好习惯
public class PickupObject : MonoBehaviour
{
virtual void Pickup(Player playerObject) { }
}
public class Coin : PickupObject
{
public int price;
override void Pickup(Player playerObject)
{
playerObject.money += price; // Move money over to the player as it probably makes more sense
}
}
public class HealthPack : PickupObject
{
public int healthRestored;
override void Pickup(Player playerObject)
{
playerObject.health += healthRestored;
}
}
public class PickupMagnet : MonoBehaviour
{
public Player PlayerObject;
private void Update()
{
PickupObject item = FindClosestItemInRange();
Pickup(item);
}
private void Pickup(PickupObject pickup)
{
pickup.Pickup(PlayerObject);
Destroy(pickup.gameObject);
}
}
编辑,关于您的代码的一些一般想法:
如果您的通用 "LivingObject" 可以像您的代码建议的那样同时获取生命值和金币,那么您可能过于通用了。听起来你只是有一个需要拾取东西的玩家。让 "anything" 能够拾取 "anything" 以我的经验来说太笼统了。不要试图在第一行代码中解决所有未来的问题。如果你不确定你要去哪里,或者在这个早期阶段结构很棘手。编写执行您需要它执行的操作的代码,并在出现模式和重复时重构它。