• 山寨一个std::bindoost::bind


    这里是最初始的版本,参考https://github.com/cplusplus-study/fork_stl/blob/master/include/bind.hpp 提供了最简洁的实现方式。

    第一部分是bind的实现代码, 第二部分是测试代码, 对bind的实现代码中有疑问或不明白的,可参考测试代码, 测试代码基本说明了某个代码的基本含义和用途。

    1. 实现

      1 ///////////////////////////////////////////////////////////////////////////////
      2 // std::bindoost::bind的山寨版本, 主要学习用.
      3 #include <stdlib.h>
      4 #include <type_traits>
      5 #include <utility>
      6 #include <tuple>
      7 #include <functional>
      8 
      9 namespace xusd{
     10     template <int NUM> struct placeholder{ };
     11 
     12     template <typename T> struct is_placeholder;
     13     template <int NUM> struct is_placeholder<placeholder<NUM> >{ enum{ value = NUM }; };
     14     template <typename T> struct is_placeholder{ enum{ value = 0 }; };
     15 
     16     template <int ...N> struct seq{ };
     17     template <unsigned N, unsigned...S> struct gen;
     18     template <unsigned N, unsigned...S> struct gen: gen<N-1, N-1, S...>{ };
     19     template <unsigned...S> struct gen<0, S...>{ typedef seq<S...> type; };
     20 
     21     template <int N, typename B, typename C>
     22     typename std::tuple_element<
     23                 N,
     24                 typename std::decay<B>::type
     25             >::type
     26     select(std::false_type, B&& b, C&& c){
     27         return std::get<N>(b);
     28     }
     29 
     30     template <int N, typename B, typename C>
     31     typename std::tuple_element<
     32                 is_placeholder<
     33                     typename std::tuple_element<
     34                         N,
     35                         typename std::decay<B>::type
     36                     >::type
     37                 >::value ==0 ? 0 :
     38                 is_placeholder<
     39                     typename std::tuple_element<
     40                         N,
     41                         typename std::decay<B>::type
     42                     >::type
     43                 >::value - 1,
     44                 typename std::decay<C>::type
     45             >::type
     46              select(std::true_type, B&& b, C&& c) 
     47     {
     48         return std::get<
     49                     is_placeholder<
     50                         typename std::tuple_element<
     51                             N,
     52                             typename std::decay<B>::type
     53                         >::type
     54                     >::value -1
     55                 >(c);
     56     }
     57 
     58     template <typename Fun> struct GetResult{
     59         typedef typename std::enable_if<
     60             std::is_class<
     61                 typename std::decay<Fun>::type
     62             >::value,
     63             typename GetResult<
     64                 decltype(&Fun::operator())
     65             >::result_type
     66         >::type result_type;
     67     };
     68 
     69     template <typename R, typename... Args>
     70     struct GetResult<R(Args...)>{
     71         typedef R result_type;
     72     };
     73     template <typename C, typename R, typename... Args>
     74     struct GetResult<R(C::*)(Args...)>{
     75         typedef R result_type;
     76     };
     77     template <typename C, typename R, typename... Args>
     78     struct GetResult<R(C::*)(Args...)const>{
     79         typedef R result_type;
     80     };
     81 
     82     template <typename C, typename R>
     83     struct GetResult<R(C::*)>{
     84         typedef decltype(((C*)0)->*((R(C::*))0)) result_type;
     85     };
     86 
     87     template<typename F, typename... Args>
     88     class bind_t {
     89         typedef std::tuple<typename std::decay<Args>::type...> BindArgs;
     90         typedef typename std::decay<F>::type CallFun;
     91         enum class BindType { MemberFunction = 0, MemberObject = 1, Other = 2 };
     92 
     93     public:
     94         typedef typename GetResult<
     95             typename std::remove_pointer<
     96                 typename std::remove_reference<F>::type
     97             >::type
     98         >::result_type result_type;
     99 
    100         bind_t(F fun, Args... args):_fun(fun), _bindArgs(args...){ }
    101 
    102         template<typename... CArgs>
    103         result_type operator()(CArgs&&... c){
    104             std::tuple<CArgs...> cargs(c...);
    105             return callFunc(
    106                     std::integral_constant<
    107                         int,
    108                         std::is_member_function_pointer<CallFun>::value ? 0 : std::is_member_object_pointer<CallFun>::value ? 1 : 2
    109                     >(),
    110                     cargs,
    111                     typename gen<
    112                         std::tuple_size<BindArgs>::value - std::is_member_function_pointer<CallFun>::value
    113                     >::type()
    114             );
    115         }
    116 
    117     private:
    118         template<typename T, int ...S>
    119         result_type callFunc(std::integral_constant<int, 2>, T&& t, seq<S...>) {
    120             return _fun(
    121                     select<S>(
    122                         std::integral_constant<
    123                             bool,
    124                             is_placeholder<
    125                                 typename std::tuple_element<S, BindArgs>::type
    126                             >::value != 0
    127                         >(),
    128                         _bindArgs,
    129                         t)...
    130             );
    131         }
    132 
    133         template<typename T, int ...S>
    134         result_type callFunc(std::integral_constant<int, 1>, T&& t, seq<S...>) {
    135             return select<0>(
    136                         std::integral_constant<
    137                             bool,
    138                             is_placeholder<
    139                                 typename std::tuple_element<
    140                                     0,
    141                                     BindArgs
    142                                 >::type
    143                             >::value != 0
    144                             >(),
    145                             _bindArgs,
    146                     t)->*_fun;
    147         }
    148 
    149         template<typename T, int ...S>
    150         result_type callFunc(std::integral_constant<int, 0>, T&& t, seq<S...>) {
    151             return (
    152                     select<0>(
    153                         std::integral_constant<
    154                             bool,
    155                             is_placeholder<
    156                                 typename std::tuple_element<
    157                                     0,
    158                                     BindArgs
    159                                 >::type
    160                             >::value != 0
    161                             >(),
    162                             _bindArgs,
    163                     t)->*_fun
    164                 )
    165                 (
    166                      select<S + 1>(
    167                          std::integral_constant<
    168                              bool,
    169                              is_placeholder<
    170                                  typename std::tuple_element<
    171                                      S + 1,
    172                                      BindArgs
    173                                  >::type
    174                              >::value != 0
    175                          >(),
    176                          _bindArgs,
    177                          t
    178                      )...
    179                 );
    180         }
    181 
    182     private:
    183         CallFun _fun;
    184         BindArgs _bindArgs;
    185     };
    186 
    187     template <typename F, typename... Args>
    188     bind_t<typename std::decay<F>::type, typename std::decay<Args&&>::type...>
    189     bind(F f, Args&&... args){
    190         return bind_t<
    191             typename std::decay<F>::type,
    192             typename std::decay<Args&&>::type...
    193         >(f, args...);
    194     }
    195 
    196     extern placeholder<1> _1;    
    197     extern placeholder<2> _2;    
    198     extern placeholder<3> _3;    
    199     extern placeholder<4> _4;
    200     extern placeholder<5> _5;    
    201     extern placeholder<6> _6;    
    202     extern placeholder<7> _7;    
    203     extern placeholder<8> _8;
    204     extern placeholder<9> _9;    
    205     extern placeholder<10> _10;    
    206     extern placeholder<11> _11;    
    207     extern placeholder<12> _12;
    208     extern placeholder<13> _13;    
    209     extern placeholder<14> _14;    
    210     extern placeholder<15> _15;    
    211     extern placeholder<16> _16;
    212     extern placeholder<17> _17;    
    213     extern placeholder<18> _18;    
    214     extern placeholder<19> _19;    
    215     extern placeholder<20> _20;
    216     extern placeholder<21> _21;    
    217     extern placeholder<22> _22;    
    218     extern placeholder<23> _23;    
    219     extern placeholder<24> _24;
    220 }

    2. 测试

    ///////////////////////////////////////////////////////////////////////////////
    // 以下开始为测试代码.
    
    #include <cstdlib>
    #include <string>
    #include <iostream>
    #include <gtest/gtest.h>
    
    int add3(int x, int y, int z){
        return x + y + z;
    }
    
    std::string to_string(std::string s1, std::string s2, std::string s3){
        return s1 + s2 + s3;
    }
    
    int g_test_voidfun =0;
    void voidfun(){
        g_test_voidfun = 1;
    }
    
    class MyTest{
    public:
        std::string to_string(std::string s1, std::string s2, std::string s3){
            return s1 + s2 + s3;
        }
        int add3(int x, int y, int z){
            return x + y + z;
        }
    
        int cadd3(int x, int y, int z) const {
            return x + y + z;
        }
        void voidfun(){
            g_test_voidfun = 2;
        }
        void constfun() const {
            g_test_voidfun = 3;
        }
    
        int memObj = 0;
        std::string memObj2;
    };
    
    class TestAddFuncter{
    public:
        int operator()(int x, int y){
            return x + y;
        }
    };
    
    
    TEST(TestSeq,Test1){
        using namespace xusd;
        EXPECT_TRUE((std::is_same<gen<0>::type, seq<> >::value));
        EXPECT_TRUE((std::is_same<gen<1>::type, seq<0> >::value));
        EXPECT_TRUE((std::is_same<gen<2>::type, seq<0,1> >::value));
        EXPECT_TRUE((std::is_same<gen<3>::type, seq<0,1,2> >::value));
        EXPECT_TRUE((std::is_same<gen<4>::type, seq<0,1,2,3> >::value));
        EXPECT_TRUE((std::is_same<gen<5>::type, seq<0,1,2,3,4> >::value));
        EXPECT_TRUE((std::is_same<gen<6>::type, seq<0,1,2,3,4,5> >::value));
        EXPECT_TRUE((std::is_same<gen<7>::type, seq<0,1,2,3,4,5,6> >::value));
        EXPECT_TRUE((std::is_same<gen<8>::type, seq<0,1,2,3,4,5,6,7> >::value));
        EXPECT_TRUE((std::is_same<gen<9>::type, seq<0,1,2,3,4,5,6,7,8> >::value));
        EXPECT_TRUE((std::is_same<gen<10>::type, seq<0,1,2,3,4,5,6,7,8,9> >::value));
    }
    
    TEST(TestPlaceHolder, Test1){
        using namespace xusd;
        EXPECT_TRUE((std::is_same<decltype(_1), placeholder<1> >::value));
        EXPECT_TRUE((std::is_same<decltype(_2), placeholder<2> >::value));
        EXPECT_TRUE((std::is_same<decltype(_3), placeholder<3> >::value));
        EXPECT_TRUE((std::is_same<decltype(_4), placeholder<4> >::value));
        EXPECT_TRUE((std::is_same<decltype(_5), placeholder<5> >::value));
        EXPECT_TRUE((std::is_same<decltype(_6), placeholder<6> >::value));
        EXPECT_TRUE((std::is_same<decltype(_7), placeholder<7> >::value));
        EXPECT_TRUE((std::is_same<decltype(_8), placeholder<8> >::value));
        EXPECT_TRUE((std::is_same<decltype(_9), placeholder<9> >::value));
    
        EXPECT_EQ(0, (is_placeholder<int>::value));
        EXPECT_EQ(0, (is_placeholder<class A>::value));
        EXPECT_EQ(1, (is_placeholder<decltype(_1)>::value));
        EXPECT_EQ(2, (is_placeholder<decltype(_2)>::value));
        EXPECT_EQ(3, (is_placeholder<decltype(_3)>::value));
        EXPECT_EQ(4, (is_placeholder<decltype(_4)>::value));
        EXPECT_EQ(5, (is_placeholder<decltype(_5)>::value));
        EXPECT_EQ(6, (is_placeholder<decltype(_6)>::value));
        EXPECT_EQ(7, (is_placeholder<decltype(_7)>::value));
        EXPECT_EQ(8, (is_placeholder<decltype(_8)>::value));
        EXPECT_EQ(9, (is_placeholder<decltype(_9)>::value));
    }
    
    TEST(TestSelectArgs, Test1){
        using namespace xusd;
        auto b = std::make_tuple(1,_1,2,_2,3,_3,4,_4);
        auto c = std::make_tuple(11,22,33,44);
    
        EXPECT_EQ(1, (select<0>(std::false_type(), b, c)));
        EXPECT_EQ(11, (select<1>(std::true_type(), b, c)));
        EXPECT_EQ(2, (select<2>(std::false_type(), b, c)));
        EXPECT_EQ(22, (select<3>(std::true_type(), b, c)));
        EXPECT_EQ(3, (select<4>(std::false_type(), b, c)));
        EXPECT_EQ(33, (select<5>(std::true_type(), b, c)));
        EXPECT_EQ(4, (select<6>(std::false_type(), b, c)));
        EXPECT_EQ(44, (select<7>(std::true_type(), b, c)));
    }
    
    TEST(TestGetResult, Test1){
        using namespace xusd;
    
        class Ret;
        class C;
        class Args;
        class Mem;
        EXPECT_TRUE((std::is_same<void,xusd::GetResult<void()>::result_type>::value));
        EXPECT_TRUE((std::is_same<int,xusd::GetResult<int(int)>::result_type>::value));
        EXPECT_TRUE((std::is_same<const int,xusd::GetResult<const int()>::result_type>::value));
        EXPECT_TRUE((std::is_same<Ret,xusd::GetResult<Ret(Args)>::result_type>::value));
        EXPECT_TRUE((std::is_same<Ret,xusd::GetResult<Ret(C::*)(Args)>::result_type>::value));
        EXPECT_TRUE((std::is_same<Mem&,xusd::GetResult<Mem(C::*)>::result_type>::value));
    
        const MyTest t1;
        EXPECT_TRUE((std::is_same<int,decltype(t1.memObj)>::value));
    }
    
    #define DT(x) decltype(x)
    TEST(TestBind_t, Test1){
        using namespace xusd;
        EXPECT_TRUE((std::is_same<void, bind_t<void ()>::result_type>::value));
        EXPECT_TRUE((std::is_same<void, bind_t<void (char), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<void, bind_t<void (char,short), DT((_1)), DT((_2))>::result_type>::value));
        EXPECT_TRUE((std::is_same<void, bind_t<void (char,short, int), DT((_1)), DT((_2)), DT((_3))>::result_type>::value));
        EXPECT_TRUE((std::is_same<void, bind_t<void (char,short, int, long), DT((_1)), DT((_2)), DT((_3)), DT((_4))>::result_type>::value));
        EXPECT_TRUE((std::is_same<void, bind_t<void (char,short, int, long, long long), DT((_1)), DT((_2)), DT((_3)), DT((_4)), DT((_5))>::result_type>::value));
    
    
        EXPECT_TRUE((std::is_same<int, bind_t<int ()>::result_type>::value));
        EXPECT_TRUE((std::is_same<int, bind_t<int (char), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<int, bind_t<int (char,short), DT((_1)), DT((_2))>::result_type>::value));
        EXPECT_TRUE((std::is_same<int, bind_t<int (char,short, int), DT((_1)), DT((_2)), DT((_3))>::result_type>::value));
        EXPECT_TRUE((std::is_same<int, bind_t<int (char,short, int, long), DT((_1)), DT((_2)), DT((_3)), DT((_4))>::result_type>::value));
        EXPECT_TRUE((std::is_same<int, bind_t<int (char,short, int, long, long long), DT((_1)), DT((_2)), DT((_3)), DT((_4)), DT((_5))>::result_type>::value));
    
    
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA ()>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (char), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (char,short), DT((_1)), DT((_2))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (char,short, int), DT((_1)), DT((_2)), DT((_3))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (char,short, int, long), DT((_1)), DT((_2)), DT((_3)), DT((_4))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (char,short, int, long, long long), DT((_1)), DT((_2)), DT((_3)), DT((_4)), DT((_5))>::result_type>::value));
    
    
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (class BBB), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (class BBB, class CCC), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (class BBB, class CCC, class DDD), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (class BBB, class CCC, class DDD, class EEE), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (class BBB, class CCC, class DDD, class EEE, class FFF), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class AAA, bind_t<class AAA (class BBB, class CCC, class DDD, class EEE, class FFF, class GGG), DT((_1))>::result_type>::value));
    
    
        EXPECT_TRUE((std::is_same<class Ret, bind_t<class Ret (class Arg1), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class Ret, bind_t<class Ret (class Arg1, class Arg2), DT((_2)), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class Ret, bind_t<class Ret (class Arg1, class Arg2, class Arg3), DT((_2)), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class Ret, bind_t<class Ret (class Arg1, class Arg2, class Arg3, class Arg4),DT((_4)), DT((_1))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class Ret, bind_t<class Ret (class Arg1, class Arg2, class Arg3, class Arg4, class Arg5), DT((_5))>::result_type>::value));
        EXPECT_TRUE((std::is_same<class Ret, bind_t<class Ret (class Arg1, class Arg2, class Arg3, class Arg4, class Arg5, class Arg6), DT((_6))>::result_type>::value));
    }
    
    TEST(TestBind_t, Test2){
        using namespace xusd;
        bind_t<int(int, int, int), DT(_3), DT(_2), DT(_1)> t1(add3, _3, _2, _1);
        EXPECT_EQ((add3(1, 2, 3)), (t1(1, 2, 3)));
        EXPECT_EQ((add3(0, 0, 0)), (t1(0, 0, 0)));
    
    
        bind_t<std::string(std::string, std::string, std::string), DT(_1), DT(_2), DT(_3)> s1(to_string, _1, _2, _3);
        bind_t<std::string(std::string, std::string, std::string), DT(_2), DT(_3), DT(_1)> s2(to_string, _2, _3, _1);
        bind_t<std::string(std::string, std::string, std::string), DT(_3), DT(_1), DT(_2)> s3(to_string, _3, _1, _2);
    
        EXPECT_EQ("123", (s1("1", "2", "3")));
        EXPECT_EQ("321", (s1("3", "2", "1")));
        EXPECT_EQ("23", (s1("", "2", "3")));
        EXPECT_EQ("2_3", (s1("2", "_", "3")));
        EXPECT_EQ("231", (s2("1", "2", "3")));
        EXPECT_EQ("312", (s3("1", "2", "3")));
    }
    
    
    
    TEST(TestBind, NotMemberFunction){
        using namespace xusd;
        EXPECT_EQ((add3(1, 2, 3)), (xusd::bind(add3, _3, _2, _1)(1, 2, 3)));
        EXPECT_EQ((add3(0, 0, 0)), (xusd::bind(add3, _3, _2, _1)(0, 0, 0)));
    
        EXPECT_EQ("123", (xusd::bind(to_string, _1, _2, _3)("1", "2", "3")));
        EXPECT_EQ("321", (xusd::bind(to_string, _1, _2, _3)("3", "2", "1")));
        EXPECT_EQ("23",  (xusd::bind(to_string, _1, _2, _3)("", "2", "3")));
        EXPECT_EQ("2_3", (xusd::bind(to_string, _1, _2, _3)("2", "_", "3")));
        EXPECT_EQ("231", (xusd::bind(to_string, _2, _3, _1)("1", "2", "3")));
        EXPECT_EQ("312", (xusd::bind(to_string, _3, _1, _2)("1", "2", "3")));
    
    
        bind((voidfun))();
        EXPECT_EQ(g_test_voidfun, 1);
    }
    
    TEST(TestBind, PassToFunctional){
        using namespace xusd;
        EXPECT_EQ("123", (std::function<std::string(std::string, std::string, std::string)>(xusd::bind(to_string, _1, _2, _3))("1", "2", "3")));
        EXPECT_EQ("321", (std::function<std::string(std::string, std::string, std::string)>(xusd::bind(to_string, _1, _2, _3))("3", "2", "1")));
        EXPECT_EQ("23",  (std::function<std::string(std::string, std::string, std::string)>(xusd::bind(to_string, _1, _2, _3))("", "2", "3")));
        EXPECT_EQ("2_3", (std::function<std::string(std::string, std::string, std::string)>(xusd::bind(to_string, _1, _2, _3))("2", "_", "3")));
        EXPECT_EQ("231", (std::function<std::string(std::string, std::string, std::string)>(xusd::bind(to_string, _2, _3, _1))("1", "2", "3")));
        EXPECT_EQ("312", (std::function<std::string(std::string, std::string, std::string)>(xusd::bind(to_string, _3, _1, _2))("1", "2", "3")));
    }
    
    
    TEST(TestBind, TestMumberFunction){
        using namespace xusd;
        MyTest test;
        EXPECT_EQ((add3(1, 2, 3)), (xusd::bind(&MyTest::add3, &test, _3, _2, _1)(1, 2, 3)));
        EXPECT_EQ((add3(0, 0, 0)), (xusd::bind(&MyTest::add3, &test, _3, _2, _1)(0, 0, 0)));
    
        EXPECT_EQ("123", (xusd::bind(&MyTest::to_string, &test, _1, _2, _3)("1", "2", "3")));
        EXPECT_EQ("321", (xusd::bind(&MyTest::to_string, &test, _1, _2, _3)("3", "2", "1")));
        EXPECT_EQ("23",  (xusd::bind(&MyTest::to_string, &test, _1, _2, _3)("", "2", "3")));
        EXPECT_EQ("2_3", (xusd::bind(&MyTest::to_string, &test, _1, _2, _3)("2", "_", "3")));
        EXPECT_EQ("231", (xusd::bind(&MyTest::to_string, &test, _2, _3, _1)("1", "2", "3")));
        EXPECT_EQ("312", (xusd::bind(&MyTest::to_string, &test, _3, _1, _2)("1", "2", "3")));
    
        EXPECT_EQ("312", (xusd::bind(&MyTest::to_string, _4, _3, _1, _2)("1", "2", "3", &test)));
    
        xusd::bind(&MyTest::voidfun, &test)();
        EXPECT_EQ(2, g_test_voidfun);
    
        xusd::bind(&MyTest::constfun, &test)();
        EXPECT_EQ(3, g_test_voidfun);
    }
    
    TEST(TestBind, TestFuncter){
        using namespace xusd;
        TestAddFuncter f1;
        EXPECT_EQ(3, (xusd::bind(f1, _1, _2)(2, 1)));
    }
    
    TEST(TestBind, TestCFunction){
        using namespace xusd;
        EXPECT_EQ(1, (xusd::bind(abs, _1)(-1)));
    }
    
    TEST(TestBind, TestMemberObj){
        MyTest t1;
        EXPECT_EQ(t1.memObj, 0);
        xusd::bind(&MyTest::memObj, &t1)() = 1;
        EXPECT_EQ(t1.memObj, 1);
    }
    
    int main(int argc, char* argv[]){
    
        ::testing::InitGoogleTest(&argc, argv);
        return RUN_ALL_TESTS();
    }
    
    namespace xusd{
        placeholder<1> _1;
        placeholder<2> _2;
        placeholder<3> _3;
        placeholder<4> _4;
        placeholder<5> _5;
        placeholder<6> _6;
        placeholder<7> _7;
        placeholder<8> _8;
        placeholder<9> _9;
        placeholder<10> _10;
        placeholder<11> _11;
        placeholder<12> _12;
        placeholder<13> _13;
        placeholder<14> _14;
        placeholder<15> _15;
        placeholder<16> _16;
        placeholder<17> _17;
        placeholder<18> _18;
        placeholder<19> _19;
        placeholder<20> _20;
        placeholder<21> _21;
        placeholder<22> _22;
        placeholder<23> _23;
        placeholder<24> _24;
    }
  • 相关阅读:
    UI控件DevExpress ASP.NET Bootstrap Controls v20.1全新升级,Accordion、GridView控件新功能更便捷!
    Web报表设计器如何使用自定义序列化程序类?DevExpress轻松搞定!
    Web界面开发工具——看Kendo UI如何实现网格最佳性能
    WPF界面解决方案首选工具,DevExpress一招搞定使用DXGrid和eXpress持久对象实现CRUD操作
    2020年界面开发有哪些新动态?Data Grid和Tree List控件使用者福音来啦!
    完整UI库Kendo UI R3 2020震撼发布|附高速下载链接
    jQuery组件库——Kendo UI轻松实现网格全球化
    PHP 开发API接口签名生成以及验证
    Nginx 配置默认读取index.php文件并设置TP路由
    PHP 借助PHPExcel做订单导出
  • 原文地址:https://www.cnblogs.com/xusd-null/p/3693817.html
Copyright © 2020-2023  润新知