为了使用C ++创建轻量级编程语言, 我们从三周前创建了标记器开始, 然后在接下来的两周中实现了表达式评估。
现在, 是时候总结并交付一种完整的编程语言, 它不像成熟的编程语言那么强大, 但是具有所有必要的功能, 包括很小的占用空间。
我觉得很有趣, 新公司的网站上有常见问题解答部分, 它们不回答经常问的问题, 而是回答他们想问的问题。我在这里也会做同样的事情。跟随我工作的人们经常问我, 为什么Stork无法编译为某些字节码或至少某些中间语言。
为什么Stork无法编译为字节码?
我很高兴回答这个问题。我的目标是开发一种占地面积小的脚本语言, 该语言可以轻松地与C ++集成。我对”小尺寸”没有严格的定义, 但是我想象一个编译器足够小, 可以移植到功能更弱的设备上, 并且在运行时不会消耗太多内存。
我并不专注于速度, 因为我认为如果你有时间紧迫的任务, 你将使用C ++进行编码, 但是如果你需要某种可扩展性, 那么像Stork这样的语言可能会有用。
我并不是说没有其他更好的语言可以完成类似的任务(例如Lua)。如果它们不存在, 那将是真正的悲剧, 我只是向你介绍这种语言的用例。
因为它将被嵌入到C ++中, 所以我发现使用C ++的某些现有功能而不是编写一个用于类似目的的整个生态系统非常方便。不仅如此, 我还发现这种方法更有趣。
与往常一样, 你可以在我的GitHub页面上找到完整的源代码。现在, 让我们仔细看看我们的进度。
变化
到目前为止, Stork还是部分完整的产品, 所以我看不到它的所有缺点和缺陷。但是, 由于它的形状较为完整, 因此我更改了前面各部分中介绍的以下内容:
- 函数不再是变量。现在, 在editor_context中有一个单独的function_lookup。为了避免混淆, 将function_param_lookup重命名为param_lookup。
- 我改变了函数的调用方式。 runtime_context中有一个调用方法, 该方法采用参数的std :: vector, 存储旧的返回值索引, 将参数推入堆栈, 更改返回值索引, 调用函数, 从堆栈中弹出参数, 恢复旧的返回值索引, 以及返回结果。这样, 我们就不必像以前那样保留返回值索引的堆栈, 因为C ++堆栈可以达到这个目的。
- 通过调用其成员函数作用域和函数返回的RAII类添加到compile_context中。这些对象中的每一个都在其构造函数中分别创建新的local_identifier_lookup和param_identifier_lookup, 并在析构函数中还原旧状态。
- 在runtime_context中添加的RAII类, 由成员函数get_scope返回。该函数将堆栈大小存储在其构造函数中, 并将其恢复到其析构函数中。
- 我一般删除了const关键字和常量对象。它们可能有用, 但并非绝对必要。
- var关键字已删除, 因为目前根本不需要它。
- 我添加了sizeof关键字, 它将在运行时检查数组大小。也许某些C ++程序员会发现名称的选择是亵渎神灵, 因为C ++ sizeof在编译时运行, 但我选择该关键字是为了避免与某些常见的变量名(例如size)冲突。
- 我添加了tostring关键字, 该关键字将任何内容显式转换为字符串。它不能是函数, 因为我们不允许函数重载。
- 各种不太有趣的更改。
语法
由于我们使用的语法与C及其相关的编程语言非常相似, 因此我将为你提供可能不清楚的细节。
变量类型声明如下:
- void, 仅用于函数返回类型
- Number
- String
- T []是包含T类型元素的数组
- R(P1, …, Pn)是一个返回R类型并接收P1到Pn类型参数的函数。如果这些类型通过引用传递, 则可以在&之前加上前缀。
函数声明如下:[public]函数R名称(P1 p1, … Pn pn)
因此, 它必须以函数为前缀。如果以public为前缀, 则可以从C ++调用它。如果该函数不返回该值, 它将求值为其返回类型的默认值。
我们允许在第一个表达式中使用声明进行for循环。我们还允许带有初始化表达式的if语句和switch语句, 如C ++ 17。 if语句从一个if块开始, 然后是零个或多个elif块, 然后是一个else块。如果在if语句的初始化表达式中声明了该变量, 则在每个这些块中将可见该变量。
我们在break语句之后允许一个可选数字, 该数字可以从多个嵌套循环中断开。因此, 你可以具有以下代码:
for (number i = 0; i < 100; ++i) {
for(number j = 0; j < 100; ++j) {
if (rnd(100) == 0) {
break 2;
}
}
}
而且, 它将从两个循环中断开。该数字在编译时经过验证。多么酷啊?
编译器
在这一部分中添加了许多功能, 但是如果我过于详细, 我什至会失去那些仍然与我保持联系的最持久的读者。因此, 我将有意跳过故事的很大一部分-编译。
这是因为我已经在本博客系列的第一部分和第二部分中进行了描述。我当时专注于表达式, 但是编译其他东西并没有太大不同。
但是, 我将举一个例子。此代码编译while语句:
statement_ptr compile_while_statement(
compiler_context& ctx, tokens_iterator& it, possible_flow pf
)
{
parse_token_value(ctx, it, reserved_token::kw_while);
parse_token_value(ctx, it, reserved_token::open_round);
expression<number>::ptr expr = build_number_expression(ctx, it);
parse_token_value(ctx, it, reserved_token::close_round);
block_statement_ptr block = compile_block_statement(ctx, it, pf);
return create_while_statement(std::move(expr), std::move(block));
}
如你所见, 它并不复杂。它先解析, 然后(然后, 它构建一个数字表达式(我们没有布尔值), 然后解析)。
之后, 它会编译可能位于{和}内的block语句(是的, 我允许单语句块), 并最终创建while语句。
你已经熟悉前两个函数参数。第三个可能的流程显示了我们正在解析的上下文中允许的流程更改命令(continue, break, return)。如果编译语句是某个编译器类的成员函数, 则可以将该信息保留在对象中, 但是我不是猛mm类的忠实拥护者, 并且编译器肯定是这样的类。传递一个额外的参数, 尤其是一个简单的参数, 不会伤害任何人, 而且谁知道, 也许有一天我们将能够并行化代码。
我想在这里解释编译的另一个有趣方面。
如果我们要支持两个函数相互调用的情况, 则可以采用C方式:通过允许向前声明或具有两个编译阶段。
我选择了第二种方法。找到函数定义后, 我们将其类型和名称解析为名为incomplete_function的对象。然后, 通过简单地计算花括号的嵌套级别, 直到我们关闭第一个花括号为止, 我们将不加解释地跳过其主体。我们将在此过程中收集令牌, 将其保留在incomplete_function中, 然后将函数标识符添加到editor_context中。
一旦传递了整个文件, 我们将完全编译每个函数, 以便可以在运行时调用它们。这样, 每个函数都可以调用文件中的任何其他函数, 并且可以访问任何全局变量。
全局变量可以通过调用相同的函数来初始化, 一旦这些函数访问未初始化的变量, 就会立即将我们引向旧的”鸡与蛋”问题。
万一发生这种情况, 可以通过抛出runtime_exception来解决问题, 这仅仅是因为我很好。坦率地说, 访问冲突是编写此类代码所能获得的最少惩罚。
全局范围
全局范围内可以出现两种实体:
- 全局变量
- 函数
可以使用返回正确类型的表达式来初始化每个全局变量。为每个全局变量创建初始化器。
每个初始化程序都会返回左值, 因此它们可用作全局变量的构造函数。如果没有为全局变量提供表达式, 则构造默认的初始化程序。
这是runtime_context中的initialize成员函数:
void runtime_context::initialize() {
_globals.clear();
for (const auto& initializer : _initializers) {
_globals.emplace_back(initializer->evaluate(*this));
}
}
从构造函数中调用。它清除全局变量容器(可以显式调用), 以重置runtime_context状态。
如前所述, 我们需要检查是否访问未初始化的全局变量。因此, 这是全局变量访问器:
variable_ptr& runtime_context::global(int idx) {
runtime_assertion(
idx < _globals.size(), "Uninitialized global variable access"
);
return _globals[idx];
}
如果第一个参数的计算结果为false, 则runtime_assertion会抛出带有相应消息的runtime_error。
每个函数都实现为捕获单个语句的lambda, 然后使用该函数接收的runtime_context对其进行评估。
函数范围
从while语句编译中可以看到, 从block语句开始递归地调用编译器, 该语句代表整个函数的块。
这是所有语句的抽象基类:
class statement {
statement(const statement&) = delete;
void operator=(const statement&) = delete;
protected:
statement() = default;
public:
virtual flow execute(runtime_context& context) = 0;
virtual ~statement() = default;
};
除默认功能外, 唯一的函数是execute, 它在runtime_context上执行语句逻辑并返回流, 该流确定程序逻辑的下一个去向。
enum struct flow_type{
f_normal, f_break, f_continue, f_return, };
class flow {
private:
flow_type _type;
int _break_level;
flow(flow_type type, int break_level);
public:
flow_type type() const;
int break_level() const;
static flow normal_flow();
static flow break_flow(int break_level);
static flow continue_flow();
static flow return_flow();
flow consume_break();
};
静态创建器函数是不言自明的, 因此我编写了它们, 以防止非零的break_level和不同于flow_type :: f_break的类型的不合逻辑的流。
现在, consumpt_break将创建一个中断流, 该中断流的中断级别要少一个, 或者, 如果中断级别达到零, 则为正常流。
现在, 我们将检查所有语句类型:
class simple_statement: public statement {
private:
expression<void>::ptr _expr;
public:
simple_statement(expression<void>::ptr expr):
_expr(std::move(expr))
{
}
flow execute(runtime_context& context) override {
_expr->evaluate(context);
return flow::normal_flow();
}
};
在这里, simple_statement是从表达式创建的语句。可以将每个表达式编译为返回void的表达式, 以便可以从中创建simple_statement。由于break, continue或return都不是表达式的一部分, 因此simple_statement返回flow :: normal_flow()。
class block_statement: public statement {
private:
std::vector<statement_ptr> _statements;
public:
block_statement(std::vector<statement_ptr> statements):
_statements(std::move(statements))
{
}
flow execute(runtime_context& context) override {
auto _ = context.enter_scope();
for (const statement_ptr& statement : _statements) {
if (
flow f = statement->execute(context);
f.type() != flow_type::f_normal
){
return f;
}
}
return flow::normal_flow();
}
};
block_statement保留语句的std :: vector。它一一执行。如果它们每个都返回非正常流, 它将立即返回该流。它使用RAII范围对象来允许局部范围变量声明。
class local_declaration_statement: public statement {
private:
std::vector<expression<lvalue>::ptr> _decls;
public:
local_declaration_statement(std::vector<expression<lvalue>::ptr> decls):
_decls(std::move(decls))
{
}
flow execute(runtime_context& context) override {
for (const expression<lvalue>::ptr& decl : _decls) {
context.push(decl->evaluate(context));
}
return flow::normal_flow();
}
};
local_declaration_statement对创建本地变量的表达式求值, 并将新的本地变量压入堆栈。
class break_statement: public statement {
private:
int _break_level;
public:
break_statement(int break_level):
_break_level(break_level)
{
}
flow execute(runtime_context&) override {
return flow::break_flow(_break_level);
}
};
break_statement在编译时评估了中断级别。它仅返回与该中断级别相对应的流。
class continue_statement: public statement {
public:
continue_statement() = default;
flow execute(runtime_context&) override {
return flow::continue_flow();
}
};
continue_statement仅返回flow :: continue_flow()。
class return_statement: public statement {
private:
expression<lvalue>::ptr _expr;
public:
return_statement(expression<lvalue>::ptr expr) :
_expr(std::move(expr))
{
}
flow execute(runtime_context& context) override {
context.retval() = _expr->evaluate(context);
return flow::return_flow();
}
};
class return_void_statement: public statement {
public:
return_void_statement() = default;
flow execute(runtime_context&) override {
return flow::return_flow();
}
};
return_statement和return_void_statement都返回flow :: return_flow()。唯一的区别是, 前者具有在返回之前会计算为返回值的表达式。
class if_statement: public statement {
private:
std::vector<expression<number>::ptr> _exprs;
std::vector<statement_ptr> _statements;
public:
if_statement(
std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements
):
_exprs(std::move(exprs)), _statements(std::move(statements))
{
}
flow execute(runtime_context& context) override {
for (size_t i = 0; i < _exprs.size(); ++i) {
if (_exprs[i]->evaluate(context)) {
return _statements[i]->execute(context);
}
}
return _statements.back()->execute(context);
}
};
class if_declare_statement: public if_statement {
private:
std::vector<expression<lvalue>::ptr> _decls;
public:
if_declare_statement(
std::vector<expression<lvalue>::ptr> decls, std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements
):
if_statement(std::move(exprs), std::move(statements)), _decls(std::move(decls))
{
}
flow execute(runtime_context& context) override {
auto _ = context.enter_scope();
for (const expression<lvalue>::ptr& decl : _decls) {
context.push(decl->evaluate(context));
}
return if_statement::execute(context);
}
};
为一个if块, 零个或多个elif块和一个else块(可能为空)创建的if_statement对其每个表达式求值, 直到一个表达式求值为1。然后执行该块并返回执行结果。如果没有表达式求值为1, 它将返回最后一个(else)块的执行。
if_declare_statement是将声明作为if子句的第一部分的语句。它将所有声明的变量压入堆栈, 然后执行其基类(if_statement)。
class switch_statement: public statement {
private:
expression<number>::ptr _expr;
std::vector<statement_ptr> _statements;
std::unordered_map<number, size_t> _cases;
size_t _dflt;
public:
switch_statement(
expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt
):
_expr(std::move(expr)), _statements(std::move(statements)), _cases(std::move(cases)), _dflt(dflt)
{
}
flow execute(runtime_context& context) override {
auto it = _cases.find(_expr->evaluate(context));
for (
size_t idx = (it == _cases.end() ? _dflt : it->second);
idx < _statements.size();
++idx
) {
switch (flow f = _statements[idx]->execute(context); f.type()) {
case flow_type::f_normal:
break;
case flow_type::f_break:
return f.consume_break();
default:
return f;
}
}
return flow::normal_flow();
}
};
class switch_declare_statement: public switch_statement {
private:
std::vector<expression<lvalue>::ptr> _decls;
public:
switch_declare_statement(
std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt
):
_decls(std::move(decls)), switch_statement(std::move(expr), std::move(statements), std::move(cases), dflt)
{
}
flow execute(runtime_context& context) override {
auto _ = context.enter_scope();
for (const expression<lvalue>::ptr& decl : _decls) {
context.push(decl->evaluate(context));
}
return switch_statement::execute(context);
}
};
switch_statement一个一个地执行其语句, 但首先跳转到从表达式求值中获取的相应索引。如果它的任何一条语句返回非正常流, 它将立即返回该流。如果它具有flow_type :: f_break, 它将首先消耗一个中断。
switch_declare_statement允许在其标头中进行声明。这些都不允许在体内声明。
class while_statement: public statement {
private:
expression<number>::ptr _expr;
statement_ptr _statement;
public:
while_statement(expression<number>::ptr expr, statement_ptr statement):
_expr(std::move(expr)), _statement(std::move(statement))
{
}
flow execute(runtime_context& context) override {
while (_expr->evaluate(context)) {
switch (flow f = _statement->execute(context); f.type()) {
case flow_type::f_normal:
case flow_type::f_continue:
break;
case flow_type::f_break:
return f.consume_break();
case flow_type::f_return:
return f;
}
}
return flow::normal_flow();
}
};
class do_statement: public statement {
private:
expression<number>::ptr _expr;
statement_ptr _statement;
public:
do_statement(expression<number>::ptr expr, statement_ptr statement):
_expr(std::move(expr)), _statement(std::move(statement))
{
}
flow execute(runtime_context& context) override {
do {
switch (flow f = _statement->execute(context); f.type()) {
case flow_type::f_normal:
case flow_type::f_continue:
break;
case flow_type::f_break:
return f.consume_break();
case flow_type::f_return:
return f;
}
} while (_expr->evaluate(context));
return flow::normal_flow();
}
};
while_statement和do_while_statement都执行其body语句, 而其表达式的计算结果为1。如果执行返回flow_type :: f_break, 则将其消耗并返回。如果返回flow_type :: f_return, 则他们将其返回。如果正常执行或继续执行, 则它们什么也不做。
似乎继续没有任何效果。但是, 内部语句受此影响。例如, 如果它是block_statement, 那么它不会求值到最后。
我发现while_statement是用C ++ while实施的, 而do语句是用C ++ do-while实施的。
class for_statement_base: public statement {
private:
expression<number>::ptr _expr2;
expression<void>::ptr _expr3;
statement_ptr _statement;
public:
for_statement_base(
expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement
):
_expr2(std::move(expr2)), _expr3(std::move(expr3)), _statement(std::move(statement))
{
}
flow execute(runtime_context& context) override {
for (; _expr2->evaluate(context); _expr3->evaluate(context)) {
switch (flow f = _statement->execute(context); f.type()) {
case flow_type::f_normal:
case flow_type::f_continue:
break;
case flow_type::f_break:
return f.consume_break();
case flow_type::f_return:
return f;
}
}
return flow::normal_flow();
}
};
class for_statement: public for_statement_base {
private:
expression<void>::ptr _expr1;
public:
for_statement(
expression<void>::ptr expr1, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement
):
for_statement_base(
std::move(expr2), std::move(expr3), std::move(statement)
), _expr1(std::move(expr1))
{
}
flow execute(runtime_context& context) override {
_expr1->evaluate(context);
return for_statement_base::execute(context);
}
};
class for_declare_statement: public for_statement_base {
private:
std::vector<expression<lvalue>::ptr> _decls;
expression<number>::ptr _expr2;
expression<void>::ptr _expr3;
statement_ptr _statement;
public:
for_declare_statement(
std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement
):
for_statement_base(
std::move(expr2), std::move(expr3), std::move(statement)
), _decls(std::move(decls))
{
}
flow execute(runtime_context& context) override {
auto _ = context.enter_scope();
for (const expression<lvalue>::ptr& decl : _decls) {
context.push(decl->evaluate(context));
}
return for_statement_base::execute(context);
}
};
for_statement和for_statement_declare的实现方式与while_statement和do_statement类似。它们从for_statement_base类继承, 该类执行大多数逻辑。当for循环的第一部分是变量声明时, 将创建for_statement_declare。
这些都是我们拥有的语句类。它们是我们职能的基础。创建runtime_context时, 它将保留这些功能。如果使用关键字public声明该函数, 则可以按名称调用它。
到此结束了Stork的核心功能。我将描述的所有其他内容都是为了使我们的语言更有用而添加的想法。
元组
数组是同质容器, 因为它们只能包含单一类型的元素。如果我们需要异构容器, 那么结构马上就浮现在脑海。
但是, 还有更多琐碎的异构容器:元组。元组可以保留不同类型的元素, 但是必须在编译时知道它们的类型。这是Stork中元组声明的示例:
[number, string] t = {22321, "Siveric"};
这将声明数字和字符串对并对其进行初始化。
初始化列表也可以用于初始化数组。如果初始化列表中的表达式类型与变量类型不匹配, 则会发生编译器错误。
由于数组是作为variable_ptr的容器实现的, 因此我们免费获得了元组的运行时实现。这是我们确保所包含变量的类型正确的编译时间。
模组
最好是向Stork用户隐藏实现细节, 并以一种更加用户友好的方式呈现该语言。
这是可以帮助我们实现目标的课程。我在没有实现细节的情况下展示了它:
class module {
...
public:
template<typename R, typename... Args>
void add_external_function(const char* name, std::function<R(Args...)> f);
template<typename R, typename... Args>
auto create_public_function_caller(std::string name);
void load(const char* path);
bool try_load(const char* path, std::ostream* err = nullptr) noexcept;
void reset_globals();
...
};
函数load和try_load将从给定路径加载并编译Stork脚本。首先, 其中一个可以引发stork :: error, 但是第二个将捕获该错误并将其打印在输出(如果提供)上。
函数reset_globals将重新初始化全局变量。
编译前应调用函数add_external_functions和create_public_function_caller。第一个添加了可以从Stork调用的C ++函数。第二个创建可调用对象, 该对象可用于从C ++调用Stork函数。如果在Stork脚本编译期间公共函数类型与R(Args …)不匹配, 则会导致编译时错误。
我添加了几个可以添加到Stork模块的标准功能。
void add_math_functions(module& m);
void add_string_functions(module& m);
void add_trace_functions(module& m);
void add_standard_functions(module& m);
例子
这是一个Stork脚本的示例:
function void swap(number& x, number& y) {
number tmp = x;
x = y;
y = tmp;
}
function void quicksort(
number[]& arr, number begin, number end, number(number, number) comp
) {
if (end - begin < 2)
return;
number pivot = arr[end-1];
number i = begin;
for (number j = begin; j < end-1; ++j)
if (comp(arr[j], pivot))
swap(&arr[i++], &arr[j]);
swap (&arr[i], &arr[end-1]);
quicksort(&arr, begin, i, comp);
quicksort(&arr, i+1, end, comp);
}
function void sort(number[]& arr, number(number, number) comp) {
quicksort(&arr, 0, sizeof(arr), comp);
}
function number less(number x, number y) {
return x < y;
}
public function void main() {
number[] arr;
for (number i = 0; i < 100; ++i) {
arr[sizeof(arr)] = rnd(100);
}
trace(tostring(arr));
sort(&arr, less);
trace(tostring(arr));
sort(&arr, greater);
trace(tostring(arr));
}
这是C ++部分:
#include <iostream>
#include "module.hpp"
#include "standard_functions.hpp"
int main() {
std::string path = __FILE__;
path = path.substr(0, path.find_last_of("/\\") + 1) + "test.stk";
using namespace stork;
module m;
add_standard_functions(m);
m.add_external_function(
"greater", std::function<number(number, number)>([](number x, number y){
return x > y;
}
));
auto s_main = m.create_public_function_caller<void>("main");
if (m.try_load(path.c_str(), &std::cerr)) {
s_main();
}
return 0;
}
在编译之前, 将标准功能添加到模块中, 并且从Stork脚本使用功能trace和rnd。更大的功能也被添加为展示柜。
该脚本是从文件” test.stk”加载的, 该文件与” main.cpp”位于同一文件夹中(通过使用__FILE__预处理程序定义), 然后调用函数main。
在脚本中, 我们生成一个随机数组, 使用C ++编写, 使用较少的比较器以升序排序, 然后使用较大的比较器以降序排序。
你会看到, 对于任何精通C(或从C派生的编程语言)的人来说, 代码都是完全可读的。
接下来做什么?
我想在Stork中实现许多功能:
- 结构体
- 类和继承
- 模块间调用
- Lambda函数
- 动态类型的对象
时间和空间的匮乏是我们尚未实施它们的原因之一。当我在业余时间实现新功能时, 我将尝试使用新版本更新GitHub页面。
包起来
我们创建了一种新的编程语言!
在过去的六周中, 这花费了我大部分的业余时间, 但是现在我可以编写一些脚本并看到它们正在运行。这就是我最近几天所做的事情, 每次意外撞到光头时, 我都会挠头。有时, 这是一个小错误, 有时是一个讨厌的错误。但是, 在其他时候, 我感到很尴尬, 因为这是我已经与全世界分享的一个错误决定。但是每次, 我都会修复并保持编码。
在此过程中, 我了解了constexpr, 这是我以前从未使用过的。我也更加熟悉了rvalue-references和完善的转发, 以及我每天都不会遇到的C ++ 17的其他较小功能。
该代码不是完美的-我永远不会提出这样的主张-但它足够好, 并且大多数情况下都遵循良好的编程习惯。最重要的是-它有效。
决定从头开始开发一种新语言对于普通人甚至是普通程序员来说都听起来很疯狂, 但这是这样做的更多理由, 并向自己证明了自己可以做到。就像解决一个难题一样, 锻炼大脑可以保持心理健康。
在我们的日常编程中, 愚蠢的挑战是常见的, 因为我们不能只挑剔它的有趣方面, 即使有时很无聊, 也必须认真工作。如果你是专业开发人员, 那么你的首要任务就是为你的雇主提供高质量的代码, 并把食物摆在桌面上。有时, 这可以使你避免在业余时间进行编程, 并且可能削弱你早期编程学习日的热情。
如果你不必这样做, 也不要失去这种热情。如果发现有趣的事情, 即使已经完成, 也可以进行处理。你无需证明自己玩得开心的理由。
而且, 如果你可以(甚至部分地)将其纳入你的专业工作中, 对你有好处!没有多少人有这个机会。
这部分的代码将冻结在我的GitHub页面上的专用分支中。
评论前必须登录!
注册