当前位置: 首页 > news >正文

Q_ENUM Q_ENUMS Q_ENUM_NS Q_FLAG Q_FLAGS Q_FLAG_NS

使用qt5.12.0

qt中有一套meta系统,用于方便做类型检测、反射等等操作。

Q_ENUMS是最早的将enum类型纳入meta系统的宏。在qt5.5版(网上看到的)之后,主要使用Q_ENUM宏。

Q_ENUM、Q_ENUMS、Q_ENUM_NS、Q_FLAG、Q_FLAGS、Q_FLAG_NS这些宏在qt中有两重意义,一重意义是直观的作为C++的宏定义,另一重意义是作为Qt的关键字。
Q_ENUMS宏定义如下:

//QtInstallDir\Src\qtbase\src\corelib\kernel\qobjectdefs.h
#ifndef QT_ANNOTATE_CLASS
# ifndef Q_COMPILER_VARIADIC_MACROS
#  define QT_ANNOTATE_CLASS(type, x)
# else
#  define QT_ANNOTATE_CLASS(type, ...)
# endif
........
#define Q_ENUMS(x) QT_ANNOTATE_CLASS(qt_enums, x)

作为宏定义,Q_ENUMS基本没有意义。

而作为Qt的关键字(参考:Q_PLUGIN_METADATA_丘上人的博客-CSDN博客 ),在moc.exe中经过Preprocessor对象的识别和Moc对象的处理并在Moc对象中记录下相关信息,然后在Generator对象中根据相关信息生成moc_*.cpp中enum相关的必要的代码。

//QtInstallDir\Src\qtbase\src\tools\moc\util\generate_keywords.cpp
static const Keyword keywords[] = {
............
    { "Q_ENUMS", "Q_ENUMS_TOKEN" },
    { "Q_ENUM", "Q_ENUM_TOKEN" },
    { "Q_ENUM_NS", "Q_ENUM_NS_TOKEN" },
    { "Q_FLAGS", "Q_FLAGS_TOKEN" },
    { "Q_FLAG", "Q_FLAG_TOKEN" },
    { "Q_FLAG_NS", "Q_FLAG_NS_TOKEN" },
............
}

----------------------

//QtInstallDir\Src\qtbase\src\tools\moc\preprocessor.cpp
Symbols Preprocessor::tokenize(const QByteArray& input, int lineNum, Preprocessor::TokenizeMode mode)
{
..........
while (*data) {
        if (mode == TokenizeCpp || mode == TokenizeDefine) {
            int column = 0;

            const char *lexem = data;
            int state = 0;
            Token token = NOTOKEN;
            for (;;) {
                if (static_cast<signed char>(*data) < 0) {
                    ++data;
                    continue;
                }
                int nextindex = keywords[state].next;
                int next = 0;
                if (*data == keywords[state].defchar)
                    next = keywords[state].defnext;
                else if (!state || nextindex)
                    next = keyword_trans[nextindex][(int)*data];
                if (!next)
                    break;
                state = next;
                token = keywords[state].token;
                ++data;
            }

            // suboptimal, is_ident_char  should use a table
            if (keywords[state].ident && is_ident_char(*data))
                token = keywords[state].ident;

            if (token == NOTOKEN) {
                if (*data)
                    ++data;
                // an error really, but let's ignore this input
                // to not confuse moc later. However in pre-processor
                // only mode let's continue.
                if (!Preprocessor::preprocessOnly)
                    continue;
            }
..........
}

----------------------
//QtInstallDir\Src\qtbase\src\tools\moc\moc.cpp
void Moc::parse(){
.........
                case Q_ENUMS_TOKEN:
                case Q_ENUM_TOKEN:
                    parseEnumOrFlag(&def, false);
                    break;
                case Q_ENUM_NS_TOKEN:
                    error("Q_ENUM_NS can't be used in a Q_OBJECT/Q_GADGET, use Q_ENUM instead");
                    break;
                case Q_FLAGS_TOKEN:
                case Q_FLAG_TOKEN:
                    parseEnumOrFlag(&def, true);
........
}

void Moc::parseEnumOrFlag(BaseDef *def, bool isFlag)
{
    next(LPAREN);
    QByteArray identifier;
    while (test(IDENTIFIER)) {
        identifier = lexem();
        while (test(SCOPE) && test(IDENTIFIER)) {
            identifier += "::";
            identifier += lexem();
        }
        def->enumDeclarations[identifier] = isFlag;
    }
    next(RPAREN);
}


----------------------
//QtInstallDir\Src\qtbase\src\tools\moc\generator.cpp
void Generator::generateEnums(int index)
{
    if (cdef->enumDeclarations.isEmpty())
        return;

    fprintf(out, "\n // enums: name, alias, flags, count, data\n");
    index += 5 * cdef->enumList.count();
    int i;
    for (i = 0; i < cdef->enumList.count(); ++i) {
        const EnumDef &e = cdef->enumList.at(i);
        int flags = 0;
        if (cdef->enumDeclarations.value(e.name))
            flags |= EnumIsFlag;
        if (e.isEnumClass)
            flags |= EnumIsScoped;
        fprintf(out, "    %4d, %4d, 0x%.1x, %4d, %4d,\n",
                 stridx(e.name),
                 e.enumName.isNull() ? stridx(e.name) : stridx(e.enumName),
                 flags,
                 e.values.count(),
                 index);
        index += e.values.count() * 2;
    }

    fprintf(out, "\n // enum data: key, value\n");
    for (i = 0; i < cdef->enumList.count(); ++i) {
        const EnumDef &e = cdef->enumList.at(i);
        for (int j = 0; j < e.values.count(); ++j) {
            const QByteArray &val = e.values.at(j);
            QByteArray code = cdef->qualified.constData();
            if (e.isEnumClass)
                code += "::" + (e.enumName.isNull() ? e.name : e.enumName);
            code += "::" + val;
            fprintf(out, "    %4d, uint(%s),\n",
                    stridx(val), code.constData());
        }
    }
}

主要来关注一下Q_ENUMS的使用和moc_*.cpp中生成的enum相关的代码:
 

//MyProjectPath/myenum.h
class MyEnum : public QObject
{
    Q_OBJECT
public:
    explicit MyEnum(QObject *parent = nullptr);

    enum class Orientation
    {
        Up = 1,
        Down = 2,
        Left = 4,
        Right = 8,
    };
    Q_ENUMS(Orientation)		//如不使用Orientation,可省略
};

------------------
//moc_myenum.cpp

struct qt_meta_stringdata_MyEnum_t {
    QByteArrayData data[6];
    char stringdata0[38];
};
static const qt_meta_stringdata_MyEnum_t qt_meta_stringdata_MyEnum = {
    {
QT_MOC_LITERAL(0, 0, 6), // "MyEnum"
QT_MOC_LITERAL(1, 7, 11), // "Orientation"
QT_MOC_LITERAL(2, 19, 2), // "Up"
QT_MOC_LITERAL(3, 22, 4), // "Down"
QT_MOC_LITERAL(4, 27, 4), // "Left"
QT_MOC_LITERAL(5, 32, 5) // "Right"

    },
    "MyEnum\0Orientation\0Up\0Down\0Left\0Right"
};

static const uint qt_meta_data_MyEnum[] = {

 // content:
       8,       // revision
       0,       // classname
       0,    0, // classinfo
       0,    0, // methods
       0,    0, // properties
       1,   14, // enums/sets
       0,    0, // constructors
       0,       // flags
       0,       // signalCount

 // enums: name, alias, flags, count, data
       1,    1, 0x2,    4,   19,

 // enum data: key, value
       2, uint(MyEnum::Orientation::Up),
       3, uint(MyEnum::Orientation::Down),
       4, uint(MyEnum::Orientation::Left),
       5, uint(MyEnum::Orientation::Right),

       0        // eod
};

QT_INIT_METAOBJECT const QMetaObject MyEnum::staticMetaObject = { {
    &QObject::staticMetaObject,
    qt_meta_stringdata_MyEnum.data,
    qt_meta_data_MyEnum,
    qt_static_metacall,
    nullptr,
    nullptr
} };

可以看到所有的enum class Orientation的信息都存入到了qt_meta_stringdata_MyEnum和qt_meta_data_MyEnum中了 。qt_meta_stringdata_MyEnum主要记录名字,最关键的还是看qt_meta_data_MyEnum,记录了enum的数量和enum的具体类型信息。其实到此为止,meta系统就已经完成了初始化了,并将初始化信息作为这几个数组中记录的数据固定存储下来了

首先要知道,qt程序构建和编译过程是先构建,构建过程用moc生成moc_*.cpp文件,然后再是预编译(宏替换),然后再是编译。也就是说moc生成moc_*.cpp代码是在预编译之前!

程序运行起来后,先将moc_*.cpp中的几个static类型的数据初始化了,也就是说MyEnum类相关的qt_meta_stringdata_MyEnum和qt_meta_data_MyEnum、QMetaObject MyEnum::staticMetaObject三个static类型的变量在main函数运行之前就是被初始化了,后续所有的对该类的meta系统的调用都是基于这几个static类型数据进行运算的。

而具体的调用过程可以参看下面这篇文章:QMetaObjectPrivate meta Q_INVOKABLE 

所以总结下:Q_ENUMS宏具体意义就是将enum class Orientation{...}的所有信息包括具体名字在moc.exe的操作下存入meta系统中。

//QtInstallDir\Src\qtbase\src\corelib\kernel\qmetaobject.h
class Q_CORE_EXPORT QMetaEnum //将枚举类型中的名字和值进行转换的工具类。
{
public:
    Q_DECL_CONSTEXPR inline QMetaEnum() : mobj(nullptr), handle(0) {}

    const char *name() const;
    const char *enumName() const;
    bool isFlag() const;
    bool isScoped() const;

    int keyCount() const;
    const char *key(int index) const;
    int value(int index) const;

    const char *scope() const;

    int keyToValue(const char *key, bool *ok = nullptr) const;
    const char* valueToKey(int value) const;
    int keysToValue(const char * keys, bool *ok = nullptr) const;
    QByteArray valueToKeys(int value) const;

    inline const QMetaObject *enclosingMetaObject() const { return mobj; }

    inline bool isValid() const { return name() != nullptr; }

.....
}
----------------------------

//projectPath//main.cpp

static QString getStringByID(MyEnum::Orientation id)
{
    if(id<MyEnum::Orientation::Up||id>MyEnum::Orientation::Right)
    {
        return QString();
    }
    const QMetaObject obj=MyEnum::staticMetaObject;//第二步
    int index=obj.indexOfEnumerator("Orientation");//第三步
    if(index<0)
    {
        return QString();
    }
    QMetaEnum en= obj.enumerator(index);//第四步,拿到MyEnum中的Orientation的QMetaEnum对象
    return QString(en.valueToKey(static_cast<int>(id)));//返回枚举的字符串
}

int main()
{
    qDebug()<<getStringByID(MyEnum::Orientation::Up)<<endl;//输出:Up
    return 0;
}

Q_ENUM与Q_ENUMS相比,多了一个宏定义,但是使用时省去了一些操作:

//QtInstallDir\Src\qtbase\src\corelib\kernel\qobjectdefs.h
#define Q_ENUM_IMPL(ENUM) \
    friend Q_DECL_CONSTEXPR const QMetaObject *qt_getEnumMetaObject(ENUM) Q_DECL_NOEXCEPT { return &staticMetaObject; } \
    friend Q_DECL_CONSTEXPR const char *qt_getEnumName(ENUM) Q_DECL_NOEXCEPT { return #ENUM; }
#define Q_ENUM(x) Q_ENUMS(x) Q_ENUM_IMPL(x)

------------------------------------
//QtInstallDir\Src\qtbase\src\corelib\kernel\qmetaobject.h
class Q_CORE_EXPORT QMetaEnum //将枚举类型中的名字和值进行转换的工具类。
{
...........
    template<typename T> static QMetaEnum fromType() {
        Q_STATIC_ASSERT_X(QtPrivate::IsQEnumHelper<T>::Value,
                          "QMetaEnum::fromType only works with enums declared as Q_ENUM or Q_FLAG");
        const QMetaObject *metaObject = qt_getEnumMetaObject(T());
        const char *name = qt_getEnumName(T());
        return metaObject->enumerator(metaObject->indexOfEnumerator(name));
    }
.......
}
-----------------------------------
//myProjectPath/main.cpp
int main()
{
    qDebug()<<getStringByID(MyEnum::Orientation::Up)<<endl;//输出:Up
    return 0;
}

 Q_ENUM_NS 是用于结合Q_NAMESPACE来使用的。作为C++宏定义与Q_ENUM是一致的。作为Qt关键字稍微有点差别。具体使用可以参考Qt中的枚举变量,Q_ENUM,Q_FLAG,Q_NAMESPACE,Q_ENUM_NS,Q_FLAG_NS以及其他_荆楚闲人的博客-CSDN博客_qt 定义枚举

//QtInstallDir\Src\qtbase\src\corelib\kernel\qobjectdefs.h
#define Q_ENUM_NS_IMPL(ENUM) \
    inline Q_DECL_CONSTEXPR const QMetaObject *qt_getEnumMetaObject(ENUM) Q_DECL_NOEXCEPT { return &staticMetaObject; } \
    inline Q_DECL_CONSTEXPR const char *qt_getEnumName(ENUM) Q_DECL_NOEXCEPT { return #ENUM; }
#define Q_ENUM_NS(x) Q_ENUMS(x) Q_ENUM_NS_IMPL(x)

#define Q_NAMESPACE \
    extern const QMetaObject staticMetaObject; \
    QT_ANNOTATE_CLASS(qt_qnamespace, "") \
    /*end*/

QMetaObjectPrivate meta_constractors Q_INVOKABLE_丘上人的博客-CSDN博客 Q_FLAGS,其意义包含了Q_ENUMS的意义,也就是Q_ENUMS能做的Q_FLAGS也能做。在moc.exe中Q_FLAGS与Q_ENUMS的操作是一致的,两者作为C++宏定义的意义也是一样的。Q_FLAGS和Q_FLAG的差异 与 Q_ENUMS和Q_ENUM的差异是一致的,为了方便下面就讲Q_FLAG

Q_FLAG通过Q_DECLARE_FLAGS宏引入了QFlags<T>模板类,它将enum 转换成QFlags类,从而进行使enum类型数据进行一些位相关的运算并能成功返回其名称。

//projectPath/myEnum.h
class MyEnum : public QObject
{
    Q_OBJECT
public:
    explicit MyEnum(QObject *parent = nullptr);

    enum class Orientation
    {
        Up = 1,
        Down = 2,
        Left = 4,
        Right = 8,
    };
    Q_FLAGS(Orientation)		//如不使用Orientation,可省略
    Q_DECLARE_FLAGS(OrientationFlags, Orientation)
};
Q_DECLARE_OPERATORS_FOR_FLAGS(MyEnum::OrientationFlags)

-----------------------------
//moc_myenum.cpp
........
static const qt_meta_stringdata_MyEnum_t qt_meta_stringdata_MyEnum = {
    {
QT_MOC_LITERAL(0, 0, 6), // "MyEnum"
QT_MOC_LITERAL(1, 7, 11), // "Orientation"
QT_MOC_LITERAL(2, 19, 2), // "Up"
QT_MOC_LITERAL(3, 22, 4), // "Down"
QT_MOC_LITERAL(4, 27, 4), // "Left"
QT_MOC_LITERAL(5, 32, 5) // "Right"

    },
    "MyEnum\0Orientation\0Up\0Down\0Left\0Right"
};
.......
static const uint qt_meta_data_MyEnum[] = {

 // content:
       8,       // revision
       0,       // classname
       0,    0, // classinfo
       0,    0, // methods
       0,    0, // properties
       1,   14, // enums/sets
       0,    0, // constructors
       0,       // flags
       0,       // signalCount

 // enums: name, alias, flags, count, data
       1,    1, 0x3,    4,   19,

 // enum data: key, value
       2, uint(MyEnum::Orientation::Up),
       3, uint(MyEnum::Orientation::Down),
       4, uint(MyEnum::Orientation::Left),
       5, uint(MyEnum::Orientation::Right),

       0        // eod
};
......
-----------------------------
//QtInstallPath\Src\qtbase\src\corelib\global\qflags.h
template<typename Enum>
class QFlags
{
    Q_STATIC_ASSERT_X((sizeof(Enum) <= sizeof(int)),
                      "QFlags uses an int as storage, so an enum with underlying "
                      "long long will overflow.");
    Q_STATIC_ASSERT_X((std::is_enum<Enum>::value), "QFlags is only usable on enumeration types.");

    struct Private;
    typedef int (Private::*Zero);
    template <typename E> friend QDataStream &operator>>(QDataStream &, QFlags<E> &);
    template <typename E> friend QDataStream &operator<<(QDataStream &, QFlags<E>);
public:
#if defined(Q_CC_MSVC) || defined(Q_CLANG_QDOC)
    // see above for MSVC
    // the definition below is too complex for qdoc
    typedef int Int;
#else
    typedef typename std::conditional<
            std::is_unsigned<typename std::underlying_type<Enum>::type>::value,
            unsigned int,
            signed int
        >::type Int;
#endif
    typedef Enum enum_type;
    // compiler-generated copy/move ctor/assignment operators are fine!
#ifdef Q_CLANG_QDOC
    Q_DECL_CONSTEXPR inline QFlags(const QFlags &other);
    Q_DECL_CONSTEXPR inline QFlags &operator=(const QFlags &other);
#endif
    Q_DECL_CONSTEXPR inline QFlags(Enum flags) Q_DECL_NOTHROW : i(Int(flags)) {}
    Q_DECL_CONSTEXPR inline QFlags(Zero = Q_NULLPTR) Q_DECL_NOTHROW : i(0) {}
    Q_DECL_CONSTEXPR inline QFlags(QFlag flag) Q_DECL_NOTHROW : i(flag) {}

#ifdef Q_COMPILER_INITIALIZER_LISTS
    Q_DECL_CONSTEXPR inline QFlags(std::initializer_list<Enum> flags) Q_DECL_NOTHROW
        : i(initializer_list_helper(flags.begin(), flags.end())) {}
#endif

    Q_DECL_RELAXED_CONSTEXPR inline QFlags &operator&=(int mask) Q_DECL_NOTHROW { i &= mask; return *this; }
    Q_DECL_RELAXED_CONSTEXPR inline QFlags &operator&=(uint mask) Q_DECL_NOTHROW { i &= mask; return *this; }
    Q_DECL_RELAXED_CONSTEXPR inline QFlags &operator&=(Enum mask) Q_DECL_NOTHROW { i &= Int(mask); return *this; }
    Q_DECL_RELAXED_CONSTEXPR inline QFlags &operator|=(QFlags other) Q_DECL_NOTHROW { i |= other.i; return *this; }
    Q_DECL_RELAXED_CONSTEXPR inline QFlags &operator|=(Enum other) Q_DECL_NOTHROW { i |= Int(other); return *this; }
    Q_DECL_RELAXED_CONSTEXPR inline QFlags &operator^=(QFlags other) Q_DECL_NOTHROW { i ^= other.i; return *this; }
    Q_DECL_RELAXED_CONSTEXPR inline QFlags &operator^=(Enum other) Q_DECL_NOTHROW { i ^= Int(other); return *this; }

    Q_DECL_CONSTEXPR inline operator Int() const Q_DECL_NOTHROW { return i; }

    Q_DECL_CONSTEXPR inline QFlags operator|(QFlags other) const Q_DECL_NOTHROW { return QFlags(QFlag(i | other.i)); }
    Q_DECL_CONSTEXPR inline QFlags operator|(Enum other) const Q_DECL_NOTHROW { return QFlags(QFlag(i | Int(other))); }
    Q_DECL_CONSTEXPR inline QFlags operator^(QFlags other) const Q_DECL_NOTHROW { return QFlags(QFlag(i ^ other.i)); }
    Q_DECL_CONSTEXPR inline QFlags operator^(Enum other) const Q_DECL_NOTHROW { return QFlags(QFlag(i ^ Int(other))); }
    Q_DECL_CONSTEXPR inline QFlags operator&(int mask) const Q_DECL_NOTHROW { return QFlags(QFlag(i & mask)); }
    Q_DECL_CONSTEXPR inline QFlags operator&(uint mask) const Q_DECL_NOTHROW { return QFlags(QFlag(i & mask)); }
    Q_DECL_CONSTEXPR inline QFlags operator&(Enum other) const Q_DECL_NOTHROW { return QFlags(QFlag(i & Int(other))); }
    Q_DECL_CONSTEXPR inline QFlags operator~() const Q_DECL_NOTHROW { return QFlags(QFlag(~i)); }

    Q_DECL_CONSTEXPR inline bool operator!() const Q_DECL_NOTHROW { return !i; }

    Q_DECL_CONSTEXPR inline bool testFlag(Enum flag) const Q_DECL_NOTHROW { return (i & Int(flag)) == Int(flag) && (Int(flag) != 0 || i == Int(flag) ); }
    Q_DECL_RELAXED_CONSTEXPR inline QFlags &setFlag(Enum flag, bool on = true) Q_DECL_NOTHROW
    {
        return on ? (*this |= flag) : (*this &= ~Int(flag));
    }

 另外还有一个宏:Q_DECLARE_OPERATORS_FOR_FLAGS
其意义是让"|"操作更方便。
 

//QtInstallPath\Src\qtbase\src\corelib\global\qflags.h
#define Q_DECLARE_OPERATORS_FOR_FLAGS(Flags) \
Q_DECL_CONSTEXPR inline QFlags<Flags::enum_type> operator|(Flags::enum_type f1, Flags::enum_type f2) Q_DECL_NOTHROW \
{ return QFlags<Flags::enum_type>(f1) | f2; } \
Q_DECL_CONSTEXPR inline QFlags<Flags::enum_type> operator|(Flags::enum_type f1, QFlags<Flags::enum_type> f2) Q_DECL_NOTHROW \
{ return f2 | f1; } Q_DECLARE_INCOMPATIBLE_FLAGS(Flags)

---------------------------------
int main()
{
    MyEnum::Orientation o = MyEnum::Orientation::Up;
    MyEnum::OrientationFlags s = o;
    MyEnum::OrientationFlags s1 = s|o; 
//如果没有声明Q_DECLARE_OPERATORS_FOR_FLAGS(MyEnum::OrientationFlags),下面语句将会报错。
    MyEnum::OrientationFlags s2 = o|s; 
    
    return 0;
}

相关文章:

  • 国外5G行业应用产业政策分析及对我国的启示
  • 【C语言】文件输入输出操作
  • 【教3妹学算法-每日一题】竞赛题:6171. 和相等的子数组
  • 遗传算法GA求解非连续函数问题
  • 【电商营销】为什么需要从获取客户转向留住客户
  • 实战讲解Redis基础数据类型List增删改查(带Java源码)
  • 分布式消息队列RocketMQ介绍
  • Django视图层模版层全面解析全网最细的教程
  • java Map集合基本概念
  • 类与对象以及原型机制
  • IMX6ULL学习笔记(6)——通过USB OTG烧录U-Boot(MfgTool工具)
  • 牛客 NC25005 [USACO 2008 Ope S]Clear And Present Danger
  • 洛谷 P2349:金字塔 ← 链式前向星 dfs
  • Flink—窗口、时间和水印
  • Cadence OrCAD Capture 查找功能详细介绍
  • flutter的key在widget list的作用以及必要性
  • Git 使用集
  • laravel 用artisan创建自己的模板
  • LeetCode刷题——29. Divide Two Integers(Part 1靠自己)
  • Python 基础起步 (十) 什么叫函数?
  • redis学习笔记(三):列表、集合、有序集合
  • Vultr 教程目录
  • 阿里云应用高可用服务公测发布
  • 包装类对象
  • 程序员该如何有效的找工作?
  • 工作踩坑系列——https访问遇到“已阻止载入混合活动内容”
  • 构建二叉树进行数值数组的去重及优化
  • 构建工具 - 收藏集 - 掘金
  • 构造函数(constructor)与原型链(prototype)关系
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 如何在GitHub上创建个人博客
  • 使用 Node.js 的 nodemailer 模块发送邮件(支持 QQ、163 等、支持附件)
  • 一起来学SpringBoot | 第十篇:使用Spring Cache集成Redis
  • const的用法,特别是用在函数前面与后面的区别
  • 策略 : 一文教你成为人工智能(AI)领域专家
  • #{}和${}的区别?
  • (1/2)敏捷实践指南 Agile Practice Guide ([美] Project Management institute 著)
  • (13):Silverlight 2 数据与通信之WebRequest
  • (pojstep1.1.1)poj 1298(直叙式模拟)
  • (ZT)一个美国文科博士的YardLife
  • (附源码)ssm基于jsp的在线点餐系统 毕业设计 111016
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理 第13章 项目资源管理(七)
  • .htaccess 强制https 单独排除某个目录
  • .NET Core 控制台程序读 appsettings.json 、注依赖、配日志、设 IOptions
  • .Net mvc总结
  • .NET Reactor简单使用教程
  • .net refrector
  • .net 按比例显示图片的缩略图
  • .NET 设计一套高性能的弱事件机制
  • .NET微信公众号开发-2.0创建自定义菜单
  • .NET委托:一个关于C#的睡前故事
  • .net中应用SQL缓存(实例使用)
  • @manytomany 保存后数据被删除_[Windows] 数据恢复软件RStudio v8.14.179675 便携特别版...
  • @serverendpoint注解_SpringBoot 使用WebSocket打造在线聊天室(基于注解)
  • [ Linux Audio 篇 ] 音频开发入门基础知识