可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Perhaps I am demonstrating my ignorance of some oft-used feautre of C# or the .NET framework, but I would like to know if there is a natively-supported way to create a type alias like EmailAddress
which aliases string
but such that I can extend it with my own methods like bool Validate()
?
I know of the using x = Some.Type;
aliases but these are not global nor do they provide type safety, i.e. one could swap out an ordinary string
for the using alias in the current file. I would like my EmailAddress
to be its own type, independent and not interchangeable with the string
type that it shadows.
My current solution is to generate public sealed partial EmailAddress : IEquatable<EmailAddress>, IXmlSerializable
classes with a T4 template generating the boilerplate implicit string conversion operators and other such things. This is fine with me for now and gives me a lot of flexibility but at the back of my mind it seems silly that I have to generate such copious amounts of boilerplate code to do something as simple as creating a strong type alias.
Maybe this is not possible other than with code generation, but I am curious if others have attempted something similar with their designs and what your experiences have been. If nothing else, perhaps this could serve as a good use-case for such an alias feature in a hypothetical future version of C#. Thanks!
EDIT: The real value that I want out of this is to be able to get type safety with primitive types that represent different types/formats for data. For instance, an EmailAddress
and a SocialSecurityNumber
and a PhoneNumber
, all of which use string
as their underlying type but which are not interchangeable types in and of themselves. I think this gets you much more readable and self-documenting code, not to mention added benefits of more method overload possibilities that are less ambiguous.
回答1:
If you look at the .NET Framework System.Uri is the closest example that is similar to an email address. In .NET the pattern is to wrap something in a class and add constraints that way.
Adding strong typing that adds additional constraints to simple types is an interesting language feature that I believe some functional language has. I can't recall the name of the language which would let you add dimensional units such as feet to your values and do a dimensional analysis on your equations to ensure that the units matched.
回答2:
Some background on why string
is sealed:
From http://www.code-magazine.com/Article.aspx?quickid=0501091 :
Rory: Hey Jay, you mind if I ask you a
couple questions? I'm already curious
about some things. First of all, and
this was brought up at one of the MSDN
events I did this week, why is String
sealed? Note: for VB.NET programmers,
Sealed = NotInheritable.
Jay: Because we do a lot of magic
tricks in String to try and make sure
we can optimize for things like
comparisons, to make them as fast as
we possibly can. So, we're stealing
bits off of pointers and other things
in there to mark things up. Just to
give you an example, and I didn't know
this when I started, but if a String
has a hyphen or an apostrophe in it
[then] it sorts differently than if it
just has text in it, and the algorithm
for sorting it if you have a hyphen or
an apostrophe if you're doing
globally-aware sorting is pretty
complicated, so we actually mark
whether or not the string has that
type of behavior in it.
Rory: So, what you're saying is that
in the string world, if you didn't
seal String there would be a whole lot
of room for wreaking a lot of havoc if
people were trying to subclass it.
Jay: Exactly. It would change the
entire layout of the object, so we
wouldn't be able to play the tricks
that we play that pick up speed.
Here is the CodeProject article that you probably have seen before:
http://www.codeproject.com/KB/cs/expandSealed.aspx
So yeah, implicit operator is your only solution.
回答3:
Does the System.Net.Mail.MailAddress
class fit your needs, or at least "help"?
EDIT: It's not explicitly IEquatable or ISerializable, but you could easily enough add those in your own wrapper.
回答4:
It seems you have at least a reasonable C# knoledgde so my answer may seem stupid, but what you want is called "type hierarchy" and the guys who coded the String class wanted to prevent you from using this "OO feature" so they made String class sealed, that's why you won't be able to do what you want. The best approach is this you are on now: Make your own type and an implicit convertion to String.
回答5:
I think you want to use extension methods. They allow you to extend a classes functionality without creating a new derived type.
回答6:
I guess I do not get why you want to have both strong types AND implicit string conversion at the same time. For me, one rules out the other.
I tried to solve the same problem for ints (you mention int in the title, but not in the question). I found that declaring an enum gives you a type-safe integer which needs to be explicitly cast from/to int.
Update
Enums may not be intended for open sets, but can still be used in such a way. This sample is from a compilation experiment to distinguish between the ID columns of several tables in a database:
enum ProcID { Unassigned = 0 }
enum TenderID { Unassigned = 0 }
void Test()
{
ProcID p = 0;
TenderID t = 0; <-- 0 is assignable to every enum
p = (ProcID)3; <-- need to explicitly convert
if (p == t) <-- operator == cannot be applied
t = -1; <-- cannot implicitly convert
DoProc(p);
DoProc(t); <-- no overloaded method found
DoTender(t);
}
void DoProc(ProcID p)
{
}
void DoTender(TenderID t)
{
}
回答7:
I made this class to cover identical needs. This one is for the type "int" (I also have one for "string"):
public class NamedInt : IComparable<int>, IEquatable<int>
{
internal int Value { get; }
protected NamedInt() { }
protected NamedInt(int val) { Value = val; }
protected NamedInt(string val) { Value = Convert.ToInt32(val); }
public static implicit operator int (NamedInt val) { return val.Value; }
public static bool operator ==(NamedInt a, int b) { return a?.Value == b; }
public static bool operator ==(NamedInt a, NamedInt b) { return a?.Value == b?.Value; }
public static bool operator !=(NamedInt a, int b) { return !(a==b); }
public static bool operator !=(NamedInt a, NamedInt b) { return !(a==b); }
public bool Equals(int other) { return Equals(new NamedInt(other)); }
public override bool Equals(object other) {
if ((other.GetType() != GetType() && other.GetType() != typeof(string))) return false;
return Equals(new NamedInt(other.ToString()));
}
private bool Equals(NamedInt other) {
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Equals(Value, other.Value);
}
public int CompareTo(int other) { return Value - other; }
public int CompareTo(NamedInt other) { return Value - other.Value; }
public override int GetHashCode() { return Value.GetHashCode(); }
public override string ToString() { return Value.ToString(); }
}
And to consume it in your case:
public class MyStronglyTypedInt: NamedInt {
public MyStronglyTypedInt(int value) : base(value) {
// Your validation can go here
}
public static implicit operator MyStronglyTypedInt(int value) {
return new MyStronglyTypedInt(value);
}
public bool Validate() {
// Your validation can go here
}
}
If you need to be able to serialize it (Newtonsoft.Json), let me know and I'll add the code.