Как я могу объявить производные классы "оболочки", которые делают только переименования?
У меня есть два разных типа строк, которые я передаю и использую в своем коде, и они тесно связаны, но их не следует путать друг с другом. Я думал, что смогу помочь себе избежать ошибок, имея два класса, которые являются просто строками, но с разными именами, чтобы сигнатуры методов (и несовместимость типов в целом) обеспечивали семантическое значение двух разных типов строк. В то же время, я не хотел, чтобы рефакторинг от something = "foo";
в something.Value = "foo";
в сотне мест.
Первая мысль:
private class FirstKind : string { }
private class SecondKind : string { }
Идея в том, что если у меня есть
void MyMethod(FirstKind varOne, SecondKind varTwo) {...}
, а затем попробуйте позвонить с MyMethod(secondKindVar, firstKindVar);
Я бы получил ошибку компилятора.
Стена, которую я ударил: string
запечатан.
Вторая мысль: создать общий KindOf<T>
класс, который ничего не делает, кроме как принимает и выплевывает значение с помощью неявных операторов преобразования. Как это:
private class KindOf<T>
{
public T Value { get; set; }
public KindOf() { Value = default(T); }
public KindOf(T val) { Value = val; }
public static implicit operator T(KindOf<T> kindOf) { return kindOf.Value; }
public static implicit operator KindOf<T>(T value) { return new KindOf<T>(value); }
}
Таким образом, я мог бы сделать что-то вроде этого:
private class FirstKind : KindOf<string> { }
private class SecondKind : KindOf<string> { }
Стена, которую я ударил: ничего от KindOf<T>
класс, кажется, наследует: ни конструкторы, ни операторы не существуют в производных типах, то есть я должен по существу заново реализовать весь базовый класс в производных классах. Копирование кода. Тьфу.
Поэтому я чувствую, что мне здесь чего-то не хватает, и, может быть, я в сорняках. Есть идеи для того, что я пытаюсь сделать?
7 ответов
Если честно, я бы вообще не использовал неявные операторы или какое-либо наследование в этом отношении. Вся идея состоит в том, чтобы иметь метод, который принимает FirstKind
, С неявным оператором вы можете просто передать строку, и она будет преобразована; это означает, что вы не заставляете код явно указывать, что это первый / второй тип. Я бы придерживался двух классов, каждый из которых имеет только конструктор, принимающий строку и свойство только для чтения. Вы слишком усложняете это, добавляя что-то еще.
Если использование этого свойства становится раздражающим, я мог бы видеть неявное преобразование в строку из каждого из пользовательских классов, но я уверен, что неявное преобразование из строки будет вредным. Хотя я согласен с тем, что обычно следует избегать копирования / вставки кода, когда это буквально одна строка кода, вставка копии, которая подразумевает преобразование в оба класса, будет проще, эффективнее и в целом более удобной, чем попытка использования наследования или чего-либо еще. чтобы не писать это дважды.
В вашем вопросе FirstKind
а также SecondKind
типы ваших строковых переменных, и string
это аргумент типа вашего KindOf<T>
учебный класс. Другой подход (который избегает дублирования кода, требуемого другими ответами), состоит в том, чтобы изменить роль FirstKind
а также SecondKind
делая их пустыми "маркерными" классами, которые используются в качестве аргументов типа String<TKind>
тип:
public class FirstKind { }
public class SecondKind { }
public struct String<TKind>
{
private readonly string _value;
public String(string value)
{
_value = value;
}
public override string ToString()
{
return _value;
}
public static implicit operator String<TKind>(string s) // see the note below
{
return new String<TKind>(s);
}
public static implicit operator string(String<TKind> s)
{
return s.ToString();
}
public static String<TKind> operator +(String<TKind> s1, String<TKind> s2)
{
return new String<TKind>(s1.ToString() + s2.ToString());
}
public int Length
{
get { return _value.Length; }
}
// You can add methods like Substring and Trim that delegate to _value.
}
Теперь вы можете объявить MyMethod
как это:
void MyMethod(String<FirstKind> varOne, String<SecondKind> varTwo)
{
varOne += varOne; // OK
varTwo += varTwo; // OK
varOne = varTwo; // ERROR
}
Примечание: я оставил в неявном операторе преобразования из простого string
, Вы можете захотеть удалить его, заставив код явно идентифицировать вид строки: new String<FirstKind>("...")
,
Я собираюсь сделать свой комментарий ответом, потому что я думаю, что он стоит как один.
Вдобавок ко всему, вам может понадобиться реализовать неявные операторы и конструкторы на подклассах. Да, вроде как копирование кода, но по крайней мере это не использует запутанный процесс или использование наследования, когда наследование на самом деле не сообщает ничего реального (риторически, что такое "KindOf" в любом случае и действительно ли это что-то значит?) И просто чтобы уменьшить дублирование кода между классами, которые не имеют ничего общего, кроме похожего синтаксиса (но различного использования)
public class FirstKind
{
public string Value { get; private set; }
public FirstKind(string value)
{
this.Value = value;
}
public static implicit operator FirstKind(string value)
{
return new FirstKind(value);
}
public static implicit operator string(FirstKind value)
{
return value.Value;
}
}
Кроме того, вы можете захотеть сделать класс неизменным, если они должны представлять строки. (еще одна деталь реализации, которая может указывать на то, что эти объекты не должны наследоваться от повторно используемых KindOf
класс, который в вашей реализации делает их изменяемыми всегда)
Он не слишком сложен и оставляет ваш код открытым для изменений в будущем (возможно, вы захотите добавить новое свойство? Добавить код проверки?), А пользователям API действительно все равно, что что-то не так. KindOf
просто чтобы сэкономить на незначительном дублировании кода. (возможно, вы можете сделать фрагмент, чтобы сделать вашу жизнь немного проще?)
Вы можете почти использовать свой общий класс. Только неявное приведение из строки (или другого типа) в класс derrived никогда не сможет работать, потому что базовый класс не может знать, следует ли приводить строку в firstkind или secondkind. Так что эта часть должна быть скопирована, а остальная часть может остаться в базовом классе:
private class FirstKind : KindOf<string>
{
public static implicit operator FirstKind(string value) { return new FirstKind{Value = value}; }
}
private class SecondKind : KindOf<string>
{
public static implicit operator SecondKind(string value) { return new SecondKind{Value = value}; }
}
Приведение к базовому классу все еще можно выполнить из общего класса:
FirstKind Foo = "Foo"; //via implicit operator in FirstKind
string stringFoo = Foo; //via implicit operator in baseclass
Я бы просто сделал это для строк.
abstract class MyTypedString
{
protected MyTypedString(string value)
{
Value = value;
}
public string Value { get; private set; }
}
Тогда вы можете закончить этот абстрактный класс с вашим FirstKind
а также SecondKind
, Это будет держать объект неизменным, как строки. Я бы не стал давать этому какие-либо неявные операторы преобразования.
Операторы все равно будут работать, вам просто нужно сослаться на .Value
член вашего класса KindOf:
FirstKind stringFoo = new FirstKind("foo");
FirstKind stringQux = new FirstKind("qux");
// String concatenation operator
FirstKind stringTar = new FirstKind( stringFoo.Value + stringQux.Value );
Однако, как вы говорите, не будет "проверки типов", если вы смешаете их, например,
SecondKind stringBar = new SecondKind("bar");
FirstKind stringHal = new FirstKind( stringFoo.Value + stringBar.Value ); // this will succeed, even though you intend stringFoo and stringBar to be incompatible
Так как операторы являются частью интерфейса класса, вы должны заново их реализовать - однако вы можете просто обернуть реализацию вложенного класса, так что вам нужно только выполнить скучную работу, чтобы заставить это работать.
Вы можете попробовать перегрузить оператор неявного преобразования вашего класса в строку. Тогда вам не придется обновлять ссылки на.Value. Классы должны будут оставаться отдельными, и вам придется создавать конструкторы для них, которые принимают строковое значение. Лучшая практика будет держать строку readonly
так что класс становится неизменным. Подражая строковому классу, для которого он предназначен.
public class FirstKind
{
private readonly string value;
public FirstKind(string v) { this.value = v };
public static implicit operator string(FirstKind v){ return v.value; }
}
public class SecondKind
{
private readonly string value;
public SecondKind (string v) { this.value = v };
public static implicit operator string(SecondKind v){ return v.value; }
}