2023年4月

theme: channing-cyan

*以下内容为本人的学习笔记,如需要转载,请声明原文链接
微信公众号「ENG八戒」
https://mp.weixin.qq.com/s/RIztusI3uKRnoHVf0sloeg

开发者虽然主要负责工程里的开发任务,但是每个开发完毕的功能都是需要开发者自测通过的,所以经常会听到开发者提起单元测试的话题。那么今天我就带大伙一起来看看大名鼎鼎的谷歌 C++ 测试框架 GoogleTest。

本文上接《
C++ 测试框架 GoogleTest 初学者入门篇 乙
》,欢迎关注公众号【ENG八戒】查看更多精彩内容。


断言

什么是断言?断言是用来对表达式执行比较的代码块,调用时类似函数。当表达式一致时,断言返回成功,否则失败。

googletest 的断言是一组宏定义。分为 ASSERT_* 和 EXPECT_* 两种。

比如

ASSERT_EQ(1, 2);

EXPECT_EQ(1, 2);

上面用到的两个断言都是比较输入的数据是否相等。主要区别是,ASSERT_* 在失败时终止程序运行,EXPECT_* 在失败时不会终止程序运行,但是都会返回错误信息。因而测试使用 EXPECT_* 可以发现更多的问题而不会打断测试流程。

那么 ASSERT_* 断言失败时,跟在其后的语句会被忽略执行,如果其中包含对资源的释放,那么就有会出现资源泄漏的问题,断言失败报错信息会附带有堆检查错误。这时出现的资源泄漏问题,真的有必要修复码?看具体情况而定。

另外,googletest 在断言失败后除了可以返回标准错误信息,还可以附带返回自定义错误信息,使用操作符 << 添加自定义错误信息。

ASSERT_EQ(1, 2) << "1 is not equal to 2";

EXPECT_EQ(1, 2) << "1 is not equal to 2";

任何可以传递给 ostream 的数据都可以作为自定义错误信息传递给断言,比如 C 字符串、string对象。

那么,测试的基本手段就是利用断言,除了判断型的断言之外,googletest 还提供了其它类型的断言用于协助测试,比如显式成功或失败、布尔类型断言、字符串比较断言等,详情可以前往官网查看手册。

https://google.github.io/googletest/reference/assertions.html

如何测试

前面提到在 googletest 中,测试的范围分为测试套件和单个测试。测试程序可以包含多个测试套件,一个测试套件可以包含多个测试。

简单的测试一般推荐使用 TEST 宏来定义单个测试。

一般的使用方式如下

TEST(test_suite_name, test_name) {
  // test body
}

test_suite_name 是测试套件名,test_name 是单个测试的名称,书写时都应该符合 C++ 的标识符规范,而且不能包含有下划线_。更详细的命名规范可以查看下面的链接

https://google.github.io/styleguide/cppguide.html#Function_Names

那么 TEST 宏到底代表着什么?一起来看看 TEST 宏定义的源代码

#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )

#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
  test_suite_name##_##test_name##_Test

#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id)       \
  static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1,                 \
                "test_suite_name must not be empty");                          \
  static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1,                       \
                "test_name must not be empty");                                \
  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
      : public parent_class {                                                  \
   public:                                                                     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
    ~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default;  \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete;     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        const GTEST_TEST_CLASS_NAME_(test_suite_name,                          \
                                     test_name) &) = delete; /* NOLINT */      \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        GTEST_TEST_CLASS_NAME_(test_suite_name,                                \
                               test_name) &&) noexcept = delete; /* NOLINT */  \
                                                                               \
   private:                                                                    \
    void TestBody() override;                                                  \
    static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;      \
  };                                                                           \
                                                                               \
  ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name,           \
                                                    test_name)::test_info_ =   \
      ::testing::internal::MakeAndRegisterTestInfo(                            \
          #test_suite_name, #test_name, nullptr, nullptr,                      \
          ::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id),  \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),          \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),       \
          new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_(     \
              test_suite_name, test_name)>);                                   \
  void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()

#define GTEST_TEST(test_suite_name, test_name)             \
  GTEST_TEST_(test_suite_name, test_name, ::testing::Test, \
              ::testing::internal::GetTestTypeId())

#define TEST(test_suite_name, test_name) GTEST_TEST(test_suite_name, test_name)

这么多预定义处理,不妨尝试代入上面的一般使用方式,然后展开一下,展开如下

static_assert(sizeof("test_suite_name") > 1,
              "test_suite_name must not be empty");
static_assert(sizeof("test_name") > 1,
              "test_name must not be empty");
			  
class test_suite_name_test_name_Test : public ::testing::Test {
  public:
  test_suite_name_test_name_Test() = default;
  ~test_suite_name_test_name_Test() override = default;
  test_suite_name_test_name_Test(const test_suite_name_test_name_Test &) = delete;
  test_suite_name_test_name_Test & operator=(
      const test_suite_name_test_name_Test &) = delete; /* NOLINT */
  test_suite_name_test_name_Test
  (test_suite_name_test_name_Test &&) noexcept = delete;
  test_suite_name_test_name_Test & operator=(
      test_suite_name_test_name_Test &&) noexcept = delete; /* NOLINT */

  private:
  void TestBody() override;
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};

::testing::TestInfo* const test_suite_name_test_name_Test::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
        "test_suite_name", "test_name", nullptr, nullptr,
        ::testing::internal::CodeLocation(__FILE__, __LINE__),
        ::testing::internal::GetTestTypeId(),
        ::testing::internal::SuiteApiResolver<
            parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
        ::testing::internal::SuiteApiResolver<
            parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
        new ::testing::internal::TestFactoryImpl<test_suite_name_test_name_Test>);
		
void test_suite_name_test_name_Test::TestBody() {
  // test body
}

从展开后的代码,可以看到有一堆代码,最开始有两个断言 static_assert 用来判断输入的测试套件名和测试名长度是否大于1,所以要求 TEST 宏定义输入的测试套件名和测试名都不能为空。

然后基于 ::testing::Test 派生了一个类,类名是测试套件名和测试名串接后再在末尾加上 _Test。类内声明重写 TestBody() 方法。

TEST 宏定义后面的 {} 用于定义派生类的成员方法 TestBody() 的函数体,内部填写标准 C++ 的有效语句作为测试主体,当然也包含调用 googletest 提供的模块内容,注意这个代码块是没有返回值的。代码块执行的断言失败时,或者代码崩溃,则测试 test_name 失败,否则成功。

再来看个例子

int square(const int a)
{
  // ...
}

TEST(SquareTest, PositiveNos) { 
    ASSERT_EQ(0, square(0));
    ASSERT_EQ(36, square(6));
    ASSERT_EQ(324, square(18));
}
 
TEST(SquareTest, NegativeNos) {
    ASSERT_EQ(1, square(-1));
    ASSERT_EQ(100, square(-10));
}

上面定义了两个测试 PositiveNos 和 NegativeNos,都属于测试套件 SquareTest。

googletest 在设计时就指定通过测试套件来汇总测试结果,所以验证同一个逻辑功能的测试应该定义在同一个测试套件内。

测试夹具

在 googletest 里什么是测试夹具?

测试夹具这个概念是为了解决当你的同一个逻辑功能测试里,有多个测试共用测试数据或者配置的问题。

需要用到测试夹具的测试一般推荐使用 TEST_F 宏来定义单个测试。

一般的使用方式如下

TEST_F(FixtureTest, test_name) {
  // test body
}

不过,TEST_F 宏的第一个输入参数不仅仅是测试套件名称,同时也是测试夹具类名。这个测试夹具类需要自己基于类 ::testing::Test 派生实现。

class FixtureTest : public testing::Test {
protected:
void SetUp() override { ... }
void TearDown() override { ... }
// custom data
};

共用的测试数据或者配置就在这个派生类里添加即可。SetUp() 用于初始化数据和配置,TearDown() 用于卸载配置。

那么 TEST_F 宏到底代表着什么,和 TEST 宏的区别在哪?一起来看看 TEST_F 宏定义的源代码

#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )

#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
  test_suite_name##_##test_name##_Test

#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id)       \
  static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1,                 \
                "test_suite_name must not be empty");                          \
  static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1,                       \
                "test_name must not be empty");                                \
  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
      : public parent_class {                                                  \
   public:                                                                     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
    ~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default;  \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete;     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        const GTEST_TEST_CLASS_NAME_(test_suite_name,                          \
                                     test_name) &) = delete; /* NOLINT */      \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        GTEST_TEST_CLASS_NAME_(test_suite_name,                                \
                               test_name) &&) noexcept = delete; /* NOLINT */  \
                                                                               \
   private:                                                                    \
    void TestBody() override;                                                  \
    static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;      \
  };                                                                           \
                                                                               \
  ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name,           \
                                                    test_name)::test_info_ =   \
      ::testing::internal::MakeAndRegisterTestInfo(                            \
          #test_suite_name, #test_name, nullptr, nullptr,                      \
          ::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id),  \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),          \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),       \
          new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_(     \
              test_suite_name, test_name)>);                                   \
  void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()

#define GTEST_TEST_F(test_fixture, test_name)        \
  GTEST_TEST_(test_fixture, test_name, test_fixture, \
              ::testing::internal::GetTypeId<test_fixture>())

#define TEST_F(test_fixture, test_name) GTEST_TEST_F(test_fixture, test_name)

这么多预定义处理,手痒代入一般的使用方式然后展开一下,展开如下

static_assert(sizeof("FixtureTest") > 1,
              "FixtureTest must not be empty");
static_assert(sizeof("test_name") > 1,
              "test_name must not be empty");
class FixtureTest_test_name_Test : public FixtureTest {
  public:
  FixtureTest_test_name_Test() = default;
  ~FixtureTest_test_name_Test() override = default;
  FixtureTest_test_name_Test(const FixtureTest_test_name_Test &) = delete;
  FixtureTest_test_name_Test & operator=(
      const FixtureTest_test_name_Test &) = delete; /* NOLINT */
  FixtureTest_test_name_Test
  (FixtureTest_test_name_Test &&) noexcept = delete;
  FixtureTest_test_name_Test & operator=(
      FixtureTest_test_name_Test &&) noexcept = delete; /* NOLINT */
  
  private:
  void TestBody() override;
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};

::testing::TestInfo* const FixtureTest_test_name_Test::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
        #FixtureTest, #test_name, nullptr, nullptr,
        ::testing::internal::CodeLocation(__FILE__, __LINE__),
        ::testing::internal::GetTypeId<FixtureTest>(),
        ::testing::internal::SuiteApiResolver<
            FixtureTest>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
        ::testing::internal::SuiteApiResolver<
            FixtureTest>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
        new ::testing::internal::TestFactoryImpl<FixtureTest_test_name_Test>);
void FixtureTest_test_name_Test::TestBody() {
  // test body
}

从展开后的代码来看,TEST_F 和 TEST 实现基本类似,那么使用时要遵循的规则也是一样的,除了需要传入自定义的基于 ::testing::Test 派生类,并且测试套件名就是测试夹具类名。

举个例子,有个模板类 Queue 的逻辑功能需要测试,它实现了 FIFO 的数据队列管理。

template <typename E>  // E 是元素类型
class Queue {
 public:
  Queue();
  void Enqueue(const E& element); // 数据入队
  E* Dequeue();  // 数据出队,如果队列为空则返回 NULL
  size_t size() const;  // 队列数据长度
  ...
};

然后需要基于 ::testing::Test 派生一个测试夹具类 QueueTest

class QueueTest : public ::testing::Test {
 protected:
  void SetUp() override {
     q1_.Enqueue(1);
     q2_.Enqueue(2);
     q2_.Enqueue(3);
  }

  // void TearDown() override {}

  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};

夹具类 QueueTest 内定义了三个队列数据对象。SetUp() 内对数据对象初始化,q0_ 保持为空,q1_ 入队一个数据,q2_ 入队两个数据。

为什么不实现 TearDown() 呢?TearDown() 本来的设计意图是卸载配置,不是刚好可以用来清理数据吗?是的,的确可以,不过这里有个更好的选择,就是使用类析构函数来对队列清空。这里有个建议就是,能用析构函数处理的,尽量用析构函数替代 TearDown()。因为用析构函数可以确保被调用而且调用的顺序不会乱,但不是说所有情况都建议用析构函数替代 TearDown(),这里不展开了。

接着调用 TEST_F 定义两个测试,基于测试夹具类 QueueTest,测试套件名也是 QueueTest,两个测试名分别为 IsEmptyInitially 和 DequeueWorks。

TEST_F(QueueTest, IsEmptyInitially) {
  EXPECT_EQ(q0_.size(), 0);
}

TEST_F(QueueTest, DequeueWorks) {
  int* n = q0_.Dequeue();
  EXPECT_EQ(n, nullptr);

  n = q1_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 1);
  EXPECT_EQ(q1_.size(), 0);
  delete n;

  n = q2_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 2);
  EXPECT_EQ(q2_.size(), 1);
  delete n;
}

上面的这两个测试定义,都会创建 QueueTest 类对象,分别创建而且不共用,所以数据不会相互影响。

第一个测试 IsEmptyInitially,googletest 框架会先创建 QueueTest 类对象 obj,调用 SetUp() 初始化数据和配置,执行测试。这里只执行了一个 EXPECT_EQ 断言,EXPECT_* 类型的断言失败后会返回失败信息,不会终止测试程序,继续下一步测试。然后调用 TearDown() 清理,最后执行对象 obj 的析构函数释放资源并退出当前测试。

第二个测试 DequeueWorks,执行流程与上一个类似。其中测试内容包含有 ASSERT_* 类别的断言,这种断言在失败后除了会返回失败信息,还会终止测试程序。如果断言失败之后的测试已没有意义,那么适合使用 ASSERT_* 类别的断言。

测试调用过程

其它 C++ 测试框架在测试开始前,需要你把测试排列出来,但是 googletest 不需要这么麻烦。 在 googletest 框架中,定义好测试后,只需要在 main 部分执行如下代码即可。

int main(int argc, char **argv)
{
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

InitGoogleTest() 可以对程序的输入命令执行解析,基于这点可以通过命令行的方式控制测试框架的运行。

继续以上面的代码为例,大致流程如下

  1. InitGoogleTest() 初始化测试框架。
  2. RUN_ALL_TESTS() 启动测试。
  3. 查找测试套件内的测试。
  4. 保存配置标志。
  5. 创建 QueueTest 实例。
  6. 调用 QueueTest 实例的 SetUp() 初始化数据配置。
  7. 执行测试。
  8. 调用 QueueTest 实例的 TearDown() 卸载数据配置。
  9. 恢复配置标志。
  10. 重复第 3 步,直到所有测试执行完毕,

RUN_ALL_TESTS() 返回 0 表示成功,否则失败。只能在主线程里调用 RUN_ALL_TESTS()。

在一般的测试里,如果在测试运行之前不需要做一些自定义的事情,而且这些事情无法在测试夹具和测试套件的框架中表达时,main 函数这部分其实都一样,那么 googletest 就在库 gtest_main 里提供了一个很方便的入口点,也就是帮你提前写好了 main 函数,你可以省去这部分,编译的时候记得链接库 gtest_main 即可。


好了,这个系列的文章就写到这里啦。

学习可以等,时间不等人!

关注我,带你学习编程领域更多核心技能!

当我们使用Native Function的时候,除了处理一些基本的逻辑操作之外,更多的还是需要进行外部数据源和服务的对接,要么是获取相关的数据,要么是保存输出结果。这一过程在Semantic Kernel中可以被归类为Connector。

Connector更像是一种设计模式,并不像Function和Memory 一样有强制和明确的规定。所以从官方的接口定义上也没有一个 IConnector类似的东西。不过我们可以从官方示例中了解这一模式的使用方法。

以 [
Web Skills 中的 WebSearchEngineSkill
] 为例,该技能使用到了一个
IWebSearchEngineConnector
作为搜索引擎的查询服务。在构造的时候将Connector的实例注入到Skill中即可。这便是很常见的依赖注入。

image

另外一个示例是[
DocumentSkill
],使用
IDocumentConnector
实现对于文档的读写操作的,使用
IFileSystemConnector
实现对于文件的访问。

image
image

不仅仅在 Native Function中的使用,OpenAI /Azure OpenAI 的接口对接,外部MemoryStore数据库的对接,都可以算在Connector的范围之内。

这样看来,Semantic Kernel使用Connector可以连接一切。


参考资料:

  1. Connectors in Semantic Kernel | Microsoft Learn
  2. semantic-kernel/WebSearchEngineSkill.cs at main · microsoft/semantic-kernel · GitHub
  3. semantic-kernel/DocumentSkill.cs at main · microsoft/semantic-kernel · GitHub

非常感谢大家对园子的支持!正是在无数园友的支持与帮助下,园子从一个业余时间的偶然变成了一份值得持之以恒为之奋斗的事业。

非常感谢大家在
绝境求商
中提供了很多很有价值的商业化建议,我们会根据大家的建议好好规划并努力探索园子的商业化道路。

在找到与园子价值吻合的商业模式或者找到愿意支持园子商业化的投资人之前,园子将继续处于困难阶段,为了缓解这段时间的资金困难,我们开设了捐助通道。

接受捐助的主体是园子的主体公司杭州语源科技有限公司,收到的捐助费用将用于园子的日常运营与研发投入。

捐助方式1:银行转账至公司账户

  • 户名:杭州语源科技有限公司
  • 账号:33050110838700000223
  • 开户行:中国建设银行股份有限公司杭州云栖小镇支行
  • SWIFT Code: PCBCCNBJZJX
  • 转账用途:建议填写“捐助博客园”

捐助方式2:微信支付转账

注:如果您对捐助有任何疑问或者建议,欢迎发邮件至 contribution@cnblogs.com 。

引言

本文篇幅较长,且不涉及任何技术方面的代码,亦可能涉及一些浅层的哲学方面的,如不喜,求放过。

经常看到在各个程序员论坛上,总是以术成文,鲜有以道成术,而诸多同僚大多数都追求于术,却略于道,而经验之谈,却是重于术数之用,故,今日开篇总结我的程序员之路,六年下来总结的属于自己的方法论分享给大家,希望对各位有所帮助。

转眼间,入行做程序员已经六年,我已经由刚开始的小菜鸟逐渐成长为此时此刻的老菜鸡,老油条,刚毕业的时候,一个人从西安跑到了杭州,在杭州那边找到了一份net相关的工作,至此开启了我的程序员之路,犹记得刚开始的时候,我也是怂怂不懂,碰到各种问题和bug也是咨询我的同事,刚毕业,就只会一点点的c#基础,html,js,css都没有写过,由于项目是webform,多少会涉及到一些前端的知识,这块相较c#来说是一个弱项,所以我总是时不时的请教我的同事,在这样的砍怪升级的路上,我不断成长,在前年的时候,我对此总结了一套属于自己的方法论,后面的几篇文章,也会根据这个以此来展开,我整理了很丑的一幅图,有些东西没有表现在图上,但我会将我所想,一一陈述出来。

问题刨析之路

首先这个图是根据两个方向去看的,但是所有程序员都是从第一阶梯最底层开始的,刚入行开始,我们要先了解我们这个行业,这个语言的基础知识,去巩固我们的基础,这样,我们才能实现技术进阶,提升我们的编码能力,至少在某些技术层面,可以很少的去打扰同事,正所谓,基础不牢,地动山摇,基础好了,那相应的,我们在写代码的时候可以提升一些我们的编码能力,至少在基础层面的编码很少犯错,当我们的基础到了一定程度的时候,我们会了解到,学习到高级的技术,我记得当时我学会反射的时候,那个时候很开心,觉得这个东西用起来在编码的时候能解决很多问题,能动态的去实现我们想要的一个业务逻辑,当我们在这一阶梯,巩固好了我们的技术之后,便自然而然的问到了我们的下一步骤。

我们不管是进入到哪个行业,这其中必不可少的是学习,所以我们是需要一套属于自己的高效的学习能力,以应对我们在工作中,在生活中所需要实现我们的想法和目标,可能有的人觉得看视频,比较适合自己,有的人觉得看书,看纸质书,电子书,这些都只是其中的术而已,而驱动你,驱动你找到高效的学习方法方为道,而我们在学习在实践的过程中,或多或少的都会遇到一些难题,疑惑,为此我们就需要借助各种各样的工具去解决我们的难题和疑惑,那和此问题相关的论坛,或者相关网站,以及搜索引擎,就派上了用场。

当然,我们在去使用以上论坛或者搜索引擎的时候,也不能是漫无目的的去搜索,而第一步,则需要我们去深层次的去分析这个问题或者需求的来源,例如,我们的客户给我们提了一个程序上的需求,那我们应该先站在客户的角度上分析这个需求给客户带来了什么好处,以及我们如何去实现这个需求,如何能够更好的去实现这个需求,或者说我们遇到了一个bug,通过打印日志,或者去看报错的代码,去分析这个bug最根源的地方是在哪,究竟是为什么报错所导致,目前就net而言,我们如果看不到源码,我自己会使用Dnspy运行程序以此来分析,程序是在哪块代码段报的错,其中有的地方会加上自己的猜想,更好的使用自己的主观能动性,去分析这个问题这个bug,当我们找到了导致我们程序最根源的bug报错代码段的时候,可能用的是第三方库,又或者是 微软自己的。那我们就可以借助论坛,博客园,MSDN,CSDN,百度,或者google,去搜索对应的解决方案。

不过,在当今信息洪流的世界里,我们周边总是被各种各样虚假信息充斥着,导致我们有的时候并不能分辨,哪些是我们想要的,哪些和我们想要的是无关的,这块就需要展现我们的搜索能力,还有过滤能力,前几年我经常帮别人远程解决问题,经常看到有的人百度的时候,方向都不对,前后端交互明明是后端的问题,搜索的时候确是前端的关键词语,看的我很是捉急,这虽然是一个很不起眼的一个行为,但却展示出个人思维能力引导的问题,虽然那个问题的表象是在于前端,但是其根源是在后端,如果从表象去检索本质的问题,那多少是有点本末倒置,这一点就需要提升我们的检索能力,而检索能力是和第三层的表达能力,思维能力是相关的,因为这两个是对你检索问题,解决问题起了一个引导性,决定性的作用,不然就是南辕北辙。

当我们通过正确的练习,提升了我们的表达能力思维能力,那相应的检索能力也会提升,至少方向不会错,关键词不会错,同时在检索的结果集中,会包含着很多信息,这其中有的是有用的,有的是无用的,这其中,就需要我们自己过滤,我们得明白,清楚,哪些是我们想要的,哪些不是,固然试试又不犯法,但试试同样浪费时间,试试也要放在有用的试验之上,毕竟试错的时间也是成本,有些结果集中,包括的东西可能和我们遇到的问题表象是一样的,但本质是千差万别,就像当今互联网中,抖音是最火的一款短视频平台了,其中包含了各个方向,各个行业的视频,而我们被这些所充斥着,尽管会根据自己的点赞喜好推荐自己可能喜欢的作品,但有时候还会夹杂一些,不是我们所喜欢的作品,这个时候其实就是一种过滤,可能很多人不思考其不喜欢的背后,就是我们大脑对此类视频的一种过滤,而且,视频的背后也不一定是你所看到的那样,所以,在面对这些信息洪流的时候,我们得明确知道自己喜欢的是什么想要的是什么,以此来解决我们的问题,实现我们的业务需求。

如果工作几年后,就是从上往下,因为那个时候,我们更应该去建设我们自己的顶层抽象能力,而不是底层的技术能力,如果前几年没有建设好底层的技术能力认知,那顶层去做出引导的时候,我们的认知不匹配,解决问题的时候,有时候也不是更为优雅的处理方式,仅仅是对于我们而言是最优雅的处理方式。

像我个人,比较喜欢去微软的官方文档,去找寻我需要使用的某个类,某个方法的使用或者如何使用,找不到的话,也会借助百度,CSDN,博客园,等各个论坛,当然有时候也会去国外的一些网站找寻我想要的答案,如果有人说CSDN怎么样怎么样,我觉得我们的目标是在于解决问题,而不是在评判某个东西怎么样怎么样,为此我想分享一下我的观点,除去问题本身之外,服务于这个问题的都是这个问题的工具,当然,除却人体之外的也都是工具,在高深一点,除却思维之外的都是工具,包括人体本身。

方法论的形成

小时候都学过的一句话,叫人有两大宝,双手和大脑,双手会动手,大脑会思考,渐而养成了善观察,喜思考,勤动手的一个习惯,而在循环往复的过程中,经常总结,复盘,思考,便形成了属于自己的方法论,在面对问题和需求的时候,我是希望自己能够拥有一种追本朔源的能力,我不知道目前我算不算有,至少我在追求,因为在面对问题,拥有了这种能力,一眼看透问题的表象,从而找到最根本的方向,在解决问题的时候,是一种事半功倍能力,可以减少很多不必要的成本,节约时间,而在上一段中,我分享和解释了那张图,其中就包括了我自己的一个方法论,就是面对问题和需求的时候,先做一个简短的分析和猜测,并在不断的解决过程中验证这个分析和猜测,最后在通过各方面的工具,去解决这个问题,然后去更好的解决这个问题,正如上所述,第二层的能力是和第三层息息相关,甚至,第一层也和第三层息息相关。

第三层上,思维能力,表达能力,沟通能力,最重要的当属思维能力,因为上图中,其实我是想把他描述成一个立体3D金字塔的,塔尖上的应该是思维能力,因为思维能力在同层的表达能力,沟通能力,还有下层的学习能力,检索能力,过滤能力,是起了一个关键性的引导作用,总结作用,方法论的形成,也是思维能力的体现,同时第一层级的基础能力,编码能力,还有高级技术能力又依赖于第二层的各个能力,而高阶编码能力,例如所常用的设计模式,都是思维能力的体现,由此可以看出,思维能力在我们工作,生活中的一个重要性,以前读书的时候,初中,是一个比较调皮捣蛋的人,老师经常说我说话不经过大脑,而我们在人际交往,或者学习,或者生活中的各个方面,我们都离不开思维能力,思维能力给了我们一个解决问题或需求的一个方向,直接性的决定了一个问题或者一件事的走向,如果善于思考,不难发现,思考本身就是思维能力的体现。正如大学所言:知止而后有定;定而后能静;静而后能安;安而后能虑;虑而后能得。

在这个社会下,我们总是扮演不同的角色,打着不同的标签,有的标签是与生俱来的,有的是后天形成的,白天,在公司我们是员工,又或者是同事,回家我们是丈夫,或者父亲,又或者是儿子,可是什么时候是自己?我从高中开始,养成了晚上睡觉前的一段时间,把这一段时间留给自己,因为 这段时间,我才属于我,不属于任何人,在这个时间里,我会对当天的事情做一个复盘,对后面的事情做一个规划,也会放开所想,去想各种事情,最多的是关于宇宙,黑洞,或者哲学,古诗词的。但我不知道其他人是否会面对自己,给自己和自己腾出时间去复盘,规划,利用自己的思维能力去做一些自己想要做的事情。

如果善观察,喜思考,勤动手你会明白生活和工作中的很多事情,会逐渐明了很多事物的本质,可能在面对不同表象的事物或者言语,或者需求和问题的时候,根据我们以往的经验,我们的思维能力会根据当下事务的本质,给我们一个反馈,从而更方便我们去解决当下的困惑。同时我们也需要针对某一件事情进行归纳总结,和思考,就像滚雪球一样不断的充实自己,这样更能方便我们形成自己的知识库,在使用我们的方法论去知识库检索,去使用工具检索,更能提升自己,让雪球越滚越大,在不断的循环中,不断的壮大自己的直径。

说了这么多,都是抽象类的东西,可能对此有些人会嗤之以鼻,正如我和我朋友所言,有的人需要碰壁才会成长,而有的人在领悟之上就可以成长,降低自己碰壁的概率,而领悟就是思维能力的体现,正所谓借假修真,借他修己。

可持续发展

从人生的角度看,短短几十年,亦如云烟,而云烟的形成也不是突然出现,或者突然不见,都是有一个过程,而这个过程就是一个可持续发展的过程,人们常说,活到老学到老,这个学,是可持续发展的重要手段,我从小菜鸟道老菜鸡,也是一个过程,是过程,就可以形成可持续发展的方向,正如我们这个行业,是一个不断需要学习的过程,在当下这个行业,当然不仅仅局限这个行业,在大多数人眼里,自我提升式彷佛成为了一种内卷,你不学习,或者你减缓你可持续发展的脚步,并不能影响别人去可持续发展,而为此别人的自我提升并不是内卷,如果自我提升,也是内卷,那小到个人,大到民族,那又如何去进步,而内卷仅仅只是无意义的行为付出,需要得到别人的认可而已,你工作无意义加班,为了得到上级的认可,着本身就是一种内卷。

我从事这个行业,刚开始就是兴趣驱动,到现在我也喜欢,可能劲头没有那么大了,而可持续发展,是需要一种底层动力作为支撑,不管是兴趣,或者是金钱,名利,等都是一个支持,如果哪天没有坚持了,那可持续发展也就到头了,如果是看本文的IT同僚们,还是希望诸位能够多学一点知识,不管是金钱支撑,还是先兴趣爱好,都是一个动力来源。

本段说是可持续发展,其实只是某一过程的可持续发展,这个过程的可持续发展,可能下一个过程也是,不过其方向不一样,人生不也是一个过程,这个过程,你可以是金钱的可持续发展,学识的可持续发展等等,但最重要的是,要抓住可持续发展的决定性根源性的东西,并为此借助方法论,或者思维能力去实现这个过程的可持续发展。

程序和生活

在刚才提及到的方法论之中,我隐约透露着方法论,不仅可以作用于工作上,也可以应用于生活中,前提是你形成了自己的这个方法论,就像最开始那张图,他并不局限于任何一个行业,如果把第一层的编码能力换成其他,高级技术也换成其他行业共有的东西,那本身就是一个通用行业的金字塔结构,生活中遇到问题,同样可以借助问题分析,方法论,去解决现实问题,例如各种稀奇古怪的人际交往,人际交往中又出现的问题,只要你善于观察,思考,总结,归纳,我想你肯定也会形成自己的独到见解,只是看到分享的更多的是术数,便想说下自己的经验之谈,希望对大家有所帮助。


PyQt
开发中,时常需要对控件的值进行校验,如需要校验
QCheckBox
是否被选中,
QLabel
是否校验值是否为空等等。在复杂的业务场景下,这类控件如果数量很多,逐个校验就显得麻烦,需要一一获得控件名称,再调用对应的方法来判断是否被选中、是否为空等。而且开发过程中如果多控件做了增减,还需要增减校验的逻辑,那会要了老命。

此篇文章,推荐使用
__dict__
属性 + 字典映射来快速校验控件值,并且无视后面控件的增减,无需调整代码。

__dict__
是什么?


python
开发的,或多或少都接触过该属性,它是

独有的一个特性,用来保存

的一些属性,关于这个属性的相关文章,网上一抓一大把,此处不作赘述,需要说明的是,
类实例
也有自己的
__dict__
属性,而且和


__dict
不同,``类实例

dict
只保存了通过
self.xxx`所声明的属性和方法。

校验逻辑演示

接下来的演示中,仅使用
QCheckBox
(为了省事),同时要保持这些控件的名称要具备同样的特征,下面的截图中,所有的控件名称都以
checkbox
结尾。

1、使用
.ui
文件生成
.py
文件

2、简单写个入口程序

继承刚才生成的
.py
文件,在这里可以实现自己的方法,在自定义类中,先打印
__dict__
,看看有哪些值。

{
  'centralwidget': <PyQt6.QtWidgets.QWidget object at 0x00000231DD4E9D30>, 
  'gridLayout': <PyQt6.QtWidgets.QGridLayout object at 0x00000231DD4E9DC0>, 
  'c_checkbox': <PyQt6.QtWidgets.QCheckBox object at 0x00000231DD4E9E50>, 
  'd_checkbox': <PyQt6.QtWidgets.QCheckBox object at 0x00000231DD4E9EE0>, 
  'a_checkbox': <PyQt6.QtWidgets.QCheckBox object at 0x00000231DD4E9F70>, 
  'b_checkbox': <PyQt6.QtWidgets.QCheckBox object at 0x00000231DD9A3040>, 
  'f_checkbox': <PyQt6.QtWidgets.QCheckBox object at 0x00000231DD9A30D0>, 
  'e_checkbox': <PyQt6.QtWidgets.QCheckBox object at 0x00000231DD9A3160>, 
  'menubar': <PyQt6.QtWidgets.QMenuBar object at 0x00000231DD9A31F0>, 
  'statusbar': <PyQt6.QtWidgets.QStatusBar object at 0x00000231DD9A3280>
}

可以看到,它是一个字典,它包含了界面上所有的控件的名称和实例对象,这里就体现出了控件名称命名时遵守统一特征的好处了,即能望文生义,也方便处理。

3、提取所有
QCheckBox
的控件名称,构建一个校验的通用逻辑

import sys
from PyQt6.QtWidgets import QMainWindow, QApplication, QCheckBox
from ui_main import Ui_MainWindow

class MainWindow(Ui_MainWindow, QMainWindow):
    def __init__(self):
        super().__init__()
        super().setupUi(self)
        self.show()
        self.checkboxs()

    
    def checkboxs(self):
        # print(self.__dict__)

        # 增加这个字典映射是为了可以自动处理更多类型的控件值校验
        widget_mapping = {
            'QCheckBox': QCheckBox.isChecked
        }
        
        # 获取所有QCheckBox的控件名称
        boxs = [
            item for item in self.__dict__ \
                if item.endswith('_checkbox')
        ]

        # 用来保存未被勾选的控件
        un_checked = []

        for item in boxs:
            widget_instance = self.__dict__.get(item)
            widget_method = widget_mapping.get(
                self.__dict__.get(item).__class__.__name__
            )
            # 如果该QCheckBox处于未选中,则被添加到列表中
            if not widget_method(widget_instance):
                un_checked.append(item)

        print(un_checked)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())

4、逻辑分析

(1)
在代码

boxs = [
            item for item in self.__dict__ \
                if item.endswith('_checkbox')
        ]

中,通过
__dict__
提取了所有以
_checkbox
结尾的控件名称,注意,这只是字符串而已,并非控件本身。

(2)构建一个通用的控件名称和控件方法的字典映射

# 增加这个字典映射是为了可以自动处理更多类型的控件值校验
        widget_mapping = {
            'QCheckBox': QCheckBox.isChecked
        }

这么做的目的是为了让这个逻辑更具通用性,让这个逻辑兼容其他控件,简单来说,可以通过控件的名称(boxs列表)找到该控件对应的方法,因为每个控件获取值所用的方法不尽相同。

(3)遍历
boxs
列表,逐个去
widget_mapping
找对应的方法,假如这里要较多种控件的话,字典映射的优势就体现出来了。
widget_method
就是该控件获取值所要用的方法了,
QCheckBox
控件,就用
isChecked
方法来获取控件是否被选中。

(4)调用所找到的方法
widget_method
。这里之所以要把控件实例
widget_instance
传入方法中,是因为控件方法
isChecked
是单独调用的,它默认要传入
self参数
即实例本身。

运行代码看看效果

可以看到6个选项均为选中,打印结果符合该事实。


qt desinger
中默认勾选两个,再试试效果

有4个未选中,打印结果符合事实。

总结

上面的仅为演示代码,只是演示处理此类问题的逻辑,刚构建逻辑时会显得很绕,但是构建起来后就很好用了,如果再渐增控件,只要遵守控件名称命名规范,那么所增加的控件校验也无需增加校验代码,减少控件也一样。

延申用法

如果一个界面中有很多控件需要填写数值或清空数值,如:

  • 提交表单后,控件数值初始化
  • 校验不通过,需要在控件中做信息提示(诸如placeholder)

一样可以使用这个逻辑进行处理,只要前期构建好,代码复用完全不是问题。