设想这样一个场景,我们正在写一个通过代码生成markdown的脚本
markdown中行内代码的语法为`code`,相应显示为code
对应到编码中,我们可以创建一个InlineCode类维护这种markdown元素
考虑到反引号只在输出markdown时需要,且反引号内的内容可能频繁修改
所以我们用一个string成员变量_text维护反引号内的内容(code),而不是整个行内代码(`code`)
而且修改内容后,我们可能还有些其他操作,比如说检查内容语法正确性啥的whatever
反正我们现在最好把_text变成私有的,然后定义set_text,get_formatted_text成员函数读写内容
但这样代码的可读性就有点太好了 XD
如果我们只用暴露一个成员text给使用者,让text可以像一个string一样被使用,也就是它被读写时自动调用set_text,get_formatted_text就好了
C#原生支持这种代理模式,我们在C#中可以将InlineCode类实现如下
public class InlineCode
{
private string _text;
private void _notify(string old, string newer)
{ Console.WriteLine($"InlineCode.Text modified from `{old}` to `{newer}`");}
public string Text
{
get => $"`{_text}`";
set { _notify(_text, value); _text = value; }
}
public string Style, Dom;
// and may have other members, omit
public InlineCode() { }
};
C# Properties在MSLearn上的介绍
简单来说,对于一个类型为T的C#字段Prop
其get必须满足委托类型(也就是函数签名+返回值类型)为T(),set委托也必须为void(T)的形式
当读取Prop时,返回get(),当赋值Prop时,调用set(value),其中value是赋值表达式右手侧
所以上述代码中,Text属性get返回带反引号的内容,注意set的value形参的声明在语法上省略了
class Program
{
static void Main(string[] args)
{
var code = new InlineCode();
code.Text = "Rust";
Console.WriteLine(code.Text);
}
}
所以使用InlineCode类时,只用关注内部文本内容,上述代码可以得到以下期望结果
InlineCode.Text modified from `` to `Rust`
`Rust`
现在考虑以下类似机制在C++中的实现
第1版代码:
template <typename T> class Field {
using Getter = std::function<T()>;
using Setter = std::function<void(const T &)>;
Getter _get;
Setter _set;
public:
explicit Field( Getter getter = {}, Setter setter = {})
: _get{getter},
_set{setter} {}
T get() { return _get(); }
operator T() { return _get(); }
Field &operator=(const T &arg) {
_set(arg);
return *this;
}
};
创建代理类Field<T>,参考C#,将get的类型Getter定义为std::function<T()>,将get的类型Getter定义为std::function<void(const T &)>
这样,只要分别用一个_get和_set变量保存Getter/Setter
然后再定义T get()并重载它的operator T() 用于读取(_get()),重载Field &operator=(const T &arg)用于修改_set(arg)
在函数体中的使用:
int main() {
string str = "default";
auto quoted_getter = [&]( ) { return "`" + str + "`"; };
auto quoted_setter = [&](const string&s){ cout << "modified from `" << str << "` to `" << s << "`\n"; str = s;};
Field<string> quoted( quoted_getter, quoted_setter);
quoted = string{"Rust"};
cout << string{quoted} << endl;
}
// output:
//modified from `default` to `Rust`
//`Rust`
当然这里有个问题,using Setter = std::function<void(const T &)>;固定了了Setter的形参类型,我们甚至没有机会去重载一个void(T &&)版本的Setter用来区分实参的值类型
嗯,在C++里左右值引用是不同的变量类型,而在有GC的C#上,引用并无左右引用而言
所以具体地,在main()第6行,我们向Field<string>::operator=(const string&)传入了纯右值string{"Rust"},但函数内部不知情,不能移动string来节省开销
所以接下来把Setter/Getter类型而不是被代理的类型T作为模板参数
第2版代码:
template<typename Getter, typename Setter>
class Field {
Getter _get;
Setter _set;
using GetterResT = std::invoke_result_t<Getter>;
public:
explicit Field(
Getter getter = {},
Setter setter = {}
) : _get{getter}, _set{setter} {}
GetterResT get() { return _get(); }
operator GetterResT() { return _get(); }
template<typename Arg>
Field &operator=(Arg &&arg) {
_set(std::forward<Arg>(arg));
return *this;
}
};
其中
- 使用
std::invoke_result_t<Getter>获取_get()返回值的类型 operator=修改为模板函数,可以同时处理左右值了
在函数体中的使用:
int main() {
string str = "default";
auto quoted_getter = [&]( ) { return "`" + str + "`"; };
auto quoted_setter = [&](auto &&s){
cout << "modified from `" << str << "` to `" << s << "`\n";
str = std::forward<decltype(s)>(s);
};
Field quoted( quoted_getter, quoted_setter);
quoted = string{"Rust"};
cout << string{quoted} << endl;
}
// output:
//modified from `default` to `Rust`
//`Rust`
注意这里setter(_set)如果需要的话,定义为一个模板函数来处理左右值
..完成一半了,接下来考虑Field作为类中成员变量如何使用
继续使用lambda作为getter/setter就不太可行了
因为定义类时要写明Getter/Setter类别,但我们拿不到lambda类型
因此改用具名的函数对象来捕获this指针
InlineCode类在C++中可以实现如下
struct InlineCode {
private:
std::string _text;
struct TextGetter {
InlineCode *self;
std::string operator()() {
return "`" + self->_text + "`";
}
} ;
struct TextSetter {
InlineCode *self;
template<typename Str>
void operator()(Str &&s) {
cout << "modified from `" << self->_text << "` to `" << s << "`\n";
self->_text = std::forward<decltype(s)>(s);;
}
};
public:
Field<TextGetter, TextSetter> text;
InlineCode(): _text{}, text{{this}, {this}}{}
};
使用:
InlineCode code;
code.text = string{"Rust"};
cout << string{code.text} << endl;
看起来类的命名因为Getter/Setter的后缀有些乱了,那再举一个栗子优化下代码结构
假设类型SomeClass有一个属性time,getter返回带前缀的时间string,setter接收时间string或UNIX时间戳(long long)
实现如下
template<typename T> using FieldAs = Field<typename T::Getter, typename T::Setter>;struct SomeClass {
std::string _time;
struct Time {
struct Getter {
SomeClass *self;
std::string operator()() {
return string{"time here is "} + self->_time;
}
};
struct Setter {
SomeClass *self;
template<typename Arg>
void operator()(Arg &&arg) {
using ValT = std::decay_t<Arg>;
if constexpr (std::is_convertible_v<ValT, std::string_view>)
self->_time = std::forward<Arg>(arg);
else {
std::tm *tm = std::localtime(&arg);
std::stringstream ss;
ss << std::put_time(tm, "%Y-%m-%d %H:%M:%S");
self->_time = ss.str();
}
}
};
};
FieldAs<Time> time;
SomeClass() : time{{this},
{this}} {};
};
首先我们定义一个模板别名
template<typename T> using FieldAs = Field<typename T::Getter, typename T::Setter>;
这样我们先在SomeClass内部定义属性对应的类型Time,Time内部再定义Getter/Setter类型,最后使用 FieldAs<Time>定义成员变量就行,这样命名更顺眼一点
使用:
int main() {
SomeClass that;
that.time = "2023-10-02";
cout << string{that.time} << endl;
that.time = std::int64_t{1696232071};
cout << string{that.time} << endl;
}
// output:
//time here is 2023-10-02
//time here is 2023-10-02 15:34:31
Full code:
当然我认为使用Properties模式时,Getter和Setter内部逻辑不能太复杂,否则整体代码可读性太差