Pouvez-vous détecter si une valeur par défaut a été attribuée à un champ C #?
-
07-07-2019 - |
Question
Supposons que vous ayez une déclaration de classe, par exemple:
class MyClass
{
int myInt=7;
int myOtherInt;
}
Maintenant, y a-t-il un moyen dans le code générique, utilisant la réflexion (ou tout autre moyen, d'ailleurs), de déduire que myInt a une valeur par défaut attribuée, alors que myOtherInt ne le fait pas? Notez la différence entre être initialisé avec une valeur implicite explicite et être laissé à sa valeur implicite implicite (myOtherInt sera initialisé à 0 par défaut).
D'après mes propres recherches, il semble qu'il n'y ait aucun moyen de le faire - mais je pensais que je demanderais ici avant d'abandonner.
[Modifier]
Même avec les types nullable et reference, je souhaite faire la distinction entre ceux qui ont été laissés comme étant null et ceux qui ont été explicitement initialisés à null. C’est pour que je puisse dire que les champs avec un initialiseur sont "optionnels". et les autres champs sont "obligatoires". Pour le moment, je dois le faire en utilisant des attributs, ce qui me perturbe par la redondance des informations dans ce cas.
La solution
J'ai compilé votre code et le charge dans ILDASM et je l’ai obtenu
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 15 (0xf)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4.7
IL_0002: stfld int32 dummyCSharp.MyClass::myInt
IL_0007: ldarg.0
IL_0008: call instance void [mscorlib]System.Object::.ctor()
IL_000d: nop
IL_000e: ret
} // end of method MyClass::.ctor
Notez que ldc.i4.7
et stfld int32 dummyCSharp.MyClass :: myInt
semblent être des instructions pour définir les valeurs par défaut du champ myInt.
Cette affectation est donc réellement compilée en tant qu'instruction d'affectation supplémentaire dans un constructeur.
Pour détecter cette affectation, vous devrez réfléchir à la méthode de construction de la méthode constructeur de MyClass et rechercher les commandes stfld
(définir les champs?).
EDIT: si j'ajoute explicitement une affectation au constructeur:
class MyClass
{
public int myInt = 7;
public int myOtherInt;
public MyClass()
{
myOtherInt = 8;
}
}
Quand je le charge dans ILDASM, j'ai ceci:
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 24 (0x18)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4.7
IL_0002: stfld int32 dummyCSharp.MyClass::myInt
IL_0007: ldarg.0
IL_0008: call instance void [mscorlib]System.Object::.ctor()
IL_000d: nop
IL_000e: nop
IL_000f: ldarg.0
IL_0010: ldc.i4.8
IL_0011: stfld int32 dummyCSharp.MyClass::myOtherInt
IL_0016: nop
IL_0017: ret
} // end of method MyClass::.ctor
Notez que l'affectation supplémentaire sur myOtherInt que j'ai ajoutée a été ajoutée après après l'appel du constructeur de la classe Object.
IL_0008: call instance void [mscorlib]System.Object::.ctor()
Alors voilà,
Toute affectation effectuée avant l'appel du constructeur de la classe Object dans IL est une attribution de valeur par défaut.
Tout ce qui suit est une instruction à l'intérieur du code constructeur de la classe.
Un test plus approfondi devrait cependant être effectué.
p.s. c'était amusant: -)
Autres conseils
Vous pouvez envisager un int nullable pour ce comportement:
class MyClass
{
int? myInt = 7;
int? myOtherInt = null;
}
Une valeur par défaut est une valeur comme une autre. Il n’existe aucun moyen de différencier ces deux cas:
int explicitly = 0;
int implicitly;
Dans les deux cas, vous leur attribuez la valeur 0. Un sens vous évite de taper. Il n'y a pas de magie " valeur non initialisée par défaut " - Ils sont tous deux zéro. Ils travaillent pour être exactement les mêmes. Cependant, le fait que vous envisagiez même cela indique que vous êtes sérieusement hors de la piste des bonnes idées. Que faites-vous? Quel est votre besoin spécifique? Vous posez la mauvaise question;)
Voici ce que je ferais si je voulais en faire une fonctionnalité d'exécution générale. Pour les types scalaires, je créerais un attribut de valeur par défaut et l’utiliserais pour déterminer le caractère par défaut.
Voici une solution partielle à la tâche - je suis sûr que cela pourrait être mieux, mais je l'ai tout simplement assommé:
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Linq;
using System.Data;
namespace FieldAttribute
{
[global::System.AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
sealed class DefaultValueAttribute : Attribute
{
public DefaultValueAttribute(int i)
{
IntVal = i;
}
public DefaultValueAttribute(bool b)
{
BoolVal = b;
}
public int IntVal { get; set; }
public bool BoolVal { get; set; }
private static FieldInfo[] GetAttributedFields(object o, string matchName)
{
Type t = o.GetType();
FieldInfo[] fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
return fields.Where(fi => ((matchName != null && fi.Name == matchName) || matchName == null) &&
(fi.GetCustomAttributes(false).Where(attr => attr is DefaultValueAttribute)).Count() > 0).ToArray();
}
public static void SetDefaultFieldValues(object o)
{
FieldInfo[] fields = GetAttributedFields(o, null);
foreach (FieldInfo fi in fields)
{
IEnumerable<object> attrs = fi.GetCustomAttributes(false).Where(attr => attr is DefaultValueAttribute);
foreach (Attribute attr in attrs)
{
DefaultValueAttribute def = attr as DefaultValueAttribute;
Type fieldType = fi.FieldType;
if (fieldType == typeof(Boolean))
{
fi.SetValue(o, def.BoolVal);
}
if (fieldType == typeof(Int32))
{
fi.SetValue(o, def.IntVal);
}
}
}
}
public static bool HasDefaultValue(object o, string fieldName)
{
FieldInfo[] fields = GetAttributedFields(o, null);
foreach (FieldInfo fi in fields)
{
IEnumerable<object> attrs = fi.GetCustomAttributes(false).Where(attr => attr is DefaultValueAttribute);
foreach (Attribute attr in attrs)
{
DefaultValueAttribute def = attr as DefaultValueAttribute;
Type fieldType = fi.FieldType;
if (fieldType == typeof(Boolean))
{
return (Boolean)fi.GetValue(o) == def.BoolVal;
}
if (fieldType == typeof(Int32))
{
return (Int32)fi.GetValue(o) == def.IntVal;
}
}
}
return false;
}
}
class Program
{
[DefaultValue(3)]
int foo;
[DefaultValue(true)]
bool b;
public Program()
{
DefaultValueAttribute.SetDefaultFieldValues(this);
Console.WriteLine(b + " " + foo);
Console.WriteLine("b has default value? " + DefaultValueAttribute.HasDefaultValue(this, "b"));
foo = 2;
Console.WriteLine("foo has default value? " + DefaultValueAttribute.HasDefaultValue(this, "foo"));
}
static void Main(string[] args)
{
Program p = new Program();
}
}
}
Pour les types de valeur utilisant un type nullable pour les paramètres facultatifs, cela devrait fonctionner. Les chaînes peuvent également être initialisées pour se vider si elles ne sont pas facultatives.
int mandatoryInt;
int? optionalInt;
Cependant, cela me semble un peu sale, je voudrais rester avec des attributs comme un moyen clair de le faire.
Peut-être que ce n’est pas la solution la plus simple ...
Vous pouvez utiliser l'attribut de DefaultValue pour définir une valeur telle que:
Importer System.ComponentModel et System.Reflection
private int myNumber = 3;
[System.ComponentModel.DefaultValue(3)]
public int MyNumber
{
get
{
return myNumber;
}
set
{
myNumber = value;
}
}
Puis récupérez la valeur par défaut avec réflexion:
PropertyInfo prop = this.GetType().GetProperty("MyNumber");
MessageBox.Show(((DefaultValueAttribute)(prop.GetCustomAttributes(typeof(DefaultValueAttribute), true).GetValue(0))).Value.ToString());
Qu'en est-il de la création d'une structure générique contenant une valeur et un indicateur initialisé?
public struct InitializationKnown<T> {
private T m_value;
private bool m_initialized;
// the default constructor leaves m_initialized = false, m_value = default(T)
// InitializationKnown() {}
InitializationKnown(T value) : m_value(value), m_initialized(true) {}
public bool initialized {
get { return m_initialized; }
}
public static operator T (InitializationKnown that) {
return that.m_value;
}
// ... other operators including assignment go here
}
Utilisez simplement ceci à la place des membres dont vous avez besoin de connaître l’initialisation de. C’est une variante assez fondamentale d’un futur paresseux ou d’une promesse.
Cette approche utilise la propriété get / set process:
class myClass
{
#region Property: MyInt
private int _myIntDefault = 7;
private bool _myIntChanged = false;
private int _myInt;
private int MyInt
{
get
{
if (_myIntChanged)
{
return _myInt;
}
else
{
return _myIntDefault;
}
}
set
{
_myInt = value;
_myIntChanged = true;
}
}
private bool MyIntIsDefault
{
get
{
if (_myIntChanged)
{
return (_myInt == _myIntDefault);
}
else
{
return true;
}
}
}
#endregion
}
C'est beaucoup de code pour un champ - bonjour des extraits!
Vous pouvez envelopper les champs dans des propriétés privées / protégées. Si vous voulez savoir s'il a été défini ou non, vérifiez le champ privé (par exemple, _myInt.HasValue ()).
class MyClass
{
public MyClass()
{
myInt = 7;
}
int? _myInt;
protected int myInt
{
set { _myInt = value; }
get { return _myInt ?? 0; }
}
int? _myOtherInt;
protected int myOtherInt
{
set { _myOtherInt = value; }
get { return _myOtherInt ?? 0; }
}
}
Si c'est ce que vous voulez, consultez le code en bas.
C'est écrit dans Oxygene [1], espérons que ce n'est pas un problème.
[1] ou Delphi Prism comment on l'appelle maintenant
var inst1 := new Sample();
var inst2 := new Sample(X := 2);
var test1 := new DefaultValueInspector<Sample>(true);
var test2 := new DefaultValueInspector<Sample>(inst2, true);
var d := test1.DefaultValueByName["X"];
var inst1HasDefault := test1.HasDefaultValue(inst1, "X");
var inst2HasDefault := test1.HasDefaultValue(inst2, "X");
Console.WriteLine("Value: {0}; inst1HasDefault: {1}; inst2HasDefault {2}",
d, inst1HasDefault, inst2HasDefault);
d := test2.DefaultValueByName["X"];
inst1HasDefault := test2.HasDefaultValue(inst1, "X");
inst2HasDefault := test2.HasDefaultValue(inst2, "X");
Console.WriteLine("Value: {0}; inst1HasDefault: {1}; inst2HasDefault {2}",
d, inst1HasDefault, inst2HasDefault);
Sortie:
Value: 1; inst1HasDefault: True; inst2HasDefault False Value: 2; inst1HasDefault: False; inst2HasDefault True
uses
System.Collections.Generic,
System.Reflection;
type
DefaultValueInspector<T> = public class
private
method get_DefaultValueByName(memberName : String): Object;
method get_DefaultValueByMember(memberInfo : MemberInfo) : Object;
protected
class method GetMemberErrorMessage(memberName : String) : String;
method GetMember(memberName : String) : MemberInfo;
property MembersByName : Dictionary<String, MemberInfo>
:= new Dictionary<String, MemberInfo>(); readonly;
property GettersByMember : Dictionary<MemberInfo, Converter<T, Object>>
:= new Dictionary<MemberInfo, Converter<T, Object>>(); readonly;
property DefaultValuesByMember : Dictionary<MemberInfo, Object>
:= new Dictionary<MemberInfo, Object>(); readonly;
public
property UseHiddenMembers : Boolean; readonly;
property DefaultValueByName[memberName : String] : Object
read get_DefaultValueByName;
property DefaultValueByMember[memberInfo : MemberInfo] : Object
read get_DefaultValueByMember;
method GetGetMethod(memberName : String) : Converter<T, Object>;
method GetGetMethod(memberInfo : MemberInfo) : Converter<T, Object>;
method HasDefaultValue(instance : T; memberName : String) : Boolean;
method HasDefaultValue(instance : T; memberInfo : MemberInfo) : Boolean;
constructor(useHiddenMembers : Boolean);
constructor(defaultInstance : T; useHiddenMembers : Boolean);
end;
implementation
constructor DefaultValueInspector<T>(useHiddenMembers : Boolean);
begin
var ctorInfo := typeOf(T).GetConstructor([]);
constructor(ctorInfo.Invoke([]) as T, useHiddenMembers);
end;
constructor DefaultValueInspector<T>(defaultInstance : T; useHiddenMembers : Boolean);
begin
var bf := iif(useHiddenMembers,
BindingFlags.NonPublic)
or BindingFlags.Public
or BindingFlags.Instance;
for mi in typeOf(T).GetMembers(bf) do
case mi.MemberType of
MemberTypes.Field :
with matching fi := FieldInfo(mi) do
begin
MembersByName.Add(fi.Name, fi);
GettersByMember.Add(mi, obj -> fi.GetValue(obj));
end;
MemberTypes.Property :
with matching pi := PropertyInfo(mi) do
if pi.GetIndexParameters().Length = 0 then
begin
MembersByName.Add(pi.Name, pi);
GettersByMember.Add(mi, obj -> pi.GetValue(obj, nil));
end;
end;
for g in GettersByMember do
with val := g.Value(DefaultInstance) do
if assigned(val) then
DefaultValuesByMember.Add(g.Key, val);
end;
class method DefaultValueInspector<T>.GetMemberErrorMessage(memberName : String) : String;
begin
exit "The member '" + memberName + "' does not exist in type " + typeOf(T).FullName
+ " or it has indexers."
end;
method DefaultValueInspector<T>.get_DefaultValueByName(memberName : String): Object;
begin
var mi := GetMember(memberName);
DefaultValuesByMember.TryGetValue(mi, out result);
end;
method DefaultValueInspector<T>.get_DefaultValueByMember(memberInfo : MemberInfo) : Object;
begin
if not DefaultValuesByMember.TryGetValue(memberInfo, out result) then
raise new ArgumentException(GetMemberErrorMessage(memberInfo.Name),
"memberName");
end;
method DefaultValueInspector<T>.GetGetMethod(memberName : String) : Converter<T, Object>;
begin
var mi := GetMember(memberName);
exit GetGetMethod(mi);
end;
method DefaultValueInspector<T>.GetGetMethod(memberInfo : MemberInfo) : Converter<T, Object>;
begin
if not GettersByMember.TryGetValue(memberInfo, out result) then
raise new ArgumentException(GetMemberErrorMessage(memberInfo.Name),
"memberName");
end;
method DefaultValueInspector<T>.GetMember(memberName : String) : MemberInfo;
begin
if not MembersByName.TryGetValue(memberName, out result) then
raise new ArgumentException(GetMemberErrorMessage(memberName),
"memberName");
end;
method DefaultValueInspector<T>.HasDefaultValue(instance : T; memberName : String) : Boolean;
begin
var getter := GetGetMethod(memberName);
var instanceValue := getter(instance);
exit Equals(DefaultValueByName[memberName], instanceValue);
end;
method DefaultValueInspector<T>.HasDefaultValue(instance : T; memberInfo : MemberInfo) : Boolean;
begin
var getter := GetGetMethod(memberInfo);
var instanceValue := getter(instance);
exit Equals(DefaultValueByMember[memberInfo], instanceValue);
end;
Le compilateur peut être configuré pour générer un avertissement si vous essayez d'utiliser une variable avant de lui affecter une valeur. J'ai le réglage par défaut et comment il se comporte.
Est-ce que l'aide suivante:
bool isAssigned = (myOtherInt == default(int));