代码质量(单元测试+代码审查)

2023-10-31

1. 单元测试

单元测试的目的:尽早在尽量小的范围内暴露错误
错误率恒定定律,一定量的代码,必然会产生一定量的BUG
a) 刚写完一个方法就发现BUG,修改只要几分钟;方法提供给其他人使用后,再发现BUG,加上双方修改,review,再联调,预计耗时可能需要半天。
b) 提交测试之后,由测试发现,需要定位原因提交BUG,双方修改BUG,review再联调,再打包测试,然后重新测试.可能需要耗费一整天
c) 而发布到线上之后再发现问题,就不只是耗费时间成本的问题,可能造成的是资损和用户流失

单元测试是对代码进行各个分支的review和深度分析过程,是"白盒测试"过程,可以有效的在很短的时间内,发现一些关键BUG
再紧的项目都要有设计、编码、测试和发布这些环节,如果说项目紧不写单测,看起来编码阶段省了一些时间,但必然会在测试和线上花掉成倍甚至更多的时间来修复。

集成测试再全面也需要单元测试
任何东西都是有死角的,例如清洗一个机箱。如果用集成测试的概念,总有一些死角是洗不到的。这些就需要单元测试来覆盖。

把代码拷贝testcase里面,然后在testcase里面调用拷贝出来的代码
理由: 后续代码改动无法监控,case永远为true,没有意义
代码里面起spring-boot容器,把服务拉起来后,从controller层做http调用
理由: 这是集成测试,不是单元测试!

测试程序类的类名通常是固定格式的,为XXXTest 形式,其中XXX 就是 被测试程序的类名。
测试方法的名称是有固定格式的,通常为“testXXX”,其中XXX 就是被测试方法的名称。虽然在JUnit 4 版本中并没有严格规定,但是最好采用同样规范。
输出测试错误信息:中文只有在测试不通过的时候才输出
assertEquals(“输出结果和期望值不同”,“Hello World”, s);
判断两个参数的值是否相等
assertEquals
测试结果true 和false
boolean b=new UserServiceImpl().login(“Tom”, “456123”);
assertTrue(b);
测试结果是否为null assertNotNull
assertNull(userDAO);
判断两个参数是否引用同一个对象
assertSame|assertNotSame 测试单例模式

JUnit 4 版本中新增了一个方法,那就是“assertThat”方法,使用该方法可以完 成上面所讲的所有方法的功能。
public static void assertThat( [value], [matcher statement] ); 其中value 表示想要测试的变量值。matcher statement 是使用Hamcrest 匹配符来表 达的对前面变量所期望的值的声明
http://hamcrest.org/ Matchers that can be combined to create flexible expressions of intent

assertThat()常用的方法还有:
a)
assertThat( n, allOf( greaterThan(1), lessThan(15) ) ); n满足allof()里的所有条件
assertThat( n, anyOf( greaterThan(16), lessThan(8) ) );n满足anyOf()里的任意条件
assertThat( n, anything() ); n是任意值(任意值都可以通过测试)
assertThat( str, is( “ellis” ) ); str是is()里的内容
assertThat( str, not( “ellis” ) ); str不是not()里的内容
b)
assertThat( str, containsString( “ellis” ) ); str包含containsString()里的内容
assertThat( str, endsWith(“ellis” ) ); str以endsWith()里的内容结尾
assertThat( str, startsWith( “ellis” ) ); str以startsWith()里的内容开始
assertThat( n, equalTo( nExpected ) ); n与equalTo()里的内容相等
assertThat( str, equalToIgnoringCase( “ellis” ) ); str忽略大小写后与equalToIgnoringCase()里的内容相等
assertThat( str, equalToIgnoringWhiteSpace( “ellis” ) );str忽略空格后与equalToIgnoringWhiteSpace()里的内容相等
c)
assertThat( d, closeTo( 3.0, 0.3 ) );d接近于3.0,误差不超过0.3
assertThat( d, greaterThan(3.0) );d大于3.0
assertThat( d, lessThan (10.0) );d小于10.0
assertThat( d, greaterThanOrEqualTo (5.0) );d大于或等于5.0
assertThat( d, lessThanOrEqualTo (16.0) );d小于或等于16.0
d)
assertThat( map, hasEntry( “ellis”, “ellis” ) );map里有一个名为ellis的key,其值为ellis
assertThat( iterable, hasItem ( “ellis” ) );iterable(例如List)里包含值ellis
assertThat( map, hasKey ( “ellis” ) );map有一个名为ellis的key
assertThat( map, hasValue ( “ellis” ) );map里包含一个值ellis

另外,还有如下这些常用注解,使测试起来更加方便:

  1. @Ignore: 被忽略的测试方法
  2. @Before: 每一个测试方法之前运行
  3. @After: 每一个测试方法之后运行
  4. @BeforeClass: 所有测试开始之前运行
  5. @AfterClass: 所有测试结束之后运行

测试程序是否发生异常 @Test(expected=java.lang.ArithmeticException.class)
测试程序运行时间 @Test(expected=java.lang.ArithmeticException.class,timeout=100)
测试方法的初始化和销毁
“@Before”注解的方法将在每一个测试方法之前执行,
“@After”注解的方 法将在每一个测试方法之后执行。
测试类的初始化和销毁
“@BeforeClass”注解标明的方法就是一个测试类初始化方法,当执行该测试类时 将首先执行该方法。
“@AfterClass”注解标明的方法就是测试类的销毁方法,当执行完 测试类中的所有测试方法后,将执行该方法。

Alibaba Java Code Guidelines, Sonar, ErrorProne, Jacoo
powermockito是改字节码
mockito是代理 spy可以部分mock,spy方法需要使用doReturn方法才不会调用实际方法。
父类对象继承的属性可以用反射和子类对象来创建

目前针对服务端单测的实现方式
可采取
Easymock
PowerMock
Mockito
样例:

@Service
public class DemoService{
    @Autowired
    private DemoDao demoDao;

    public boolean getString(int type){
        int result = demoDao.getStringByType(type);
        if(result == 1){
            return true;
        }else {
            return false;
        }
    }
}  

public class DemoServiceTest{

    @InjectMocks
    private DemoService demoService;

    @Mock
    private DemoDao demoDao;

    @Test(dependsOnMethods = "getStringMock" )
    private void testGetString(){
        Assert.assertEquals(demoService.getString(1),true);
        Assert.assertEquals(demoService.getString(2),false);
    }

    private int getStringMock(){
        when(demoDao.getStringByType(1)).thenReturn(1);
        when(demoDao.getStringByType(2)).thenReturn(2);
    }
}

直接new XXX(),然后调用里面方法做测试验证
样例:

public class DemoUtils{

    public static boolean convert(String str) throws Exception{
        if ("true".equals(str)){
            return true;
        }else if("false".equals(str)){
            return false;
        }else {
            throw new Exception("convert fail");
        }
    } 
}  

public class DemoUtilsTest{
    @Test
    public void testConvert_true(){
        DemoUtils demoUtils = new DemoUtils();
        Assert.assertEquals(demoUtils.convert("true"),true);
    }

    @Test
    public void testConvert_false(){
        DemoUtils demoUtils = new DemoUtils();
        Assert.assertEquals(demoUtils.convert("false"),false);
    }

    @Test
    public void testConvert_exception(){
        DemoUtils demoUtils = new DemoUtils();
        try{
            demoUtils.convert("exception");
             Assert.assertTrue(false); // 异常用例走不到这里,若走到这里,则失败
        }catch (Exception e){
            Assert.assertEquals(e.getMessage(),"convert fail");
        }
    }
}

不可采取

错误示例1 ##

public class PatternUtilsTest {
    @Test
    public void test1() {
        String content = "<td class=\"weight\" style=\"color:red;\">12.00</td></tr>\t\t\t";
        System.out.println(PatternUtils.group(content, "style=\"color:red;\">(.*?)<\\/td><\\/tr>", 1));
    }
}

总结:
没有assert
没有调用项目代码,只是一段调试代码,调试自己的正则而已,对代码没有一点监控作用

正确写法:
调用项目中使用到这段正则的方法(mock或者直接new都可以)
assert正则匹配后的结果

错误示例 ##

public class MysqlAdaptServiceTest {
@Test(dataProvider = "telSheet")
    public void testConvertTelSheetToVoiceRecord(TelSheet telSheet) {
        VoiceCallRecord voiceCallRecord = service.convertTelSheetToVoiceRecord(telSheet);
        assertEquals(voiceCallRecord.getTime(), telSheet.getCallStart());
        assertEquals(voiceCallRecord.getDialtype(), telSheet.getCallType() == 1 ? "主叫" :
                telSheet.getCallType() == 2 ? "被叫" : telSheet.getCallType() == 3 ? "呼叫转移" : "未知");
        assertEquals(voiceCallRecord.getDurationsec(), telSheet.getCallSeconds());
        assertEquals(voiceCallRecord.getLocation(), telSheet.getCallAddress());
        assertEquals(voiceCallRecord.getLocationtype(), telSheet.getTelType());
        assertEquals(voiceCallRecord.getPeernumber(), telSheet.getOtherNumber());
        assertEquals(voiceCallRecord.getCreatetime(), telSheet.getCreateTime());
        assertEquals(voiceCallRecord.getLastmodifytime(), telSheet.getLastModifyTime());
    }
}

总结:
代码不要对预期数据(expect)做任何处理

assertEquals(voiceCallRecord.getDialtype(), telSheet.getCallType() == 1 ? “主叫” :
telSheet.getCallType() == 2 ? “被叫” : telSheet.getCallType() == 3 ? “呼叫转移” : “未知”);
这个assert中,把代码处理逻辑搬到testcase中,试问,假如这个地方失败了,是代码里面的转换错了呢?还是预期结果的转换处理错了呢?
所以,不要对预期结果做任何的改动
正确写法:

在预期结果里面,增加 callTypeName字段,分别构造4个case,覆盖"主叫",“被叫”,“呼叫转移”,"未知"的场景
assert的时候,直接取预期结果和转化后的做对比

错误示例2 ##

public class PhoneQueryServiceTest {
    @SuppressWarnings("unchecked")
    @DataProvider
    public Object[][] voiceCallRecord() throws IOException {
        List<VoiceCallRecord> voiceCallRecordList = objectMapper.readValue(ClassLoader.getSystemResource("PhoneQueryService/voice-call-record.json"), new TypeReference<List<VoiceCallRecord>>() {
        });
        List<Map<String, Object>> mapList = objectMapper.readValue(ClassLoader.getSystemResource("PhoneQueryService/voice-call-record.json"), List.class);
        Object[][] data = new Object[voiceCallRecordList.size()][1];
        for (int i = 0, size = voiceCallRecordList.size(); i < size; i++) {
            voiceCallRecordList.get(i).setPhonenumberid(UUID.fromString((String) mapList.get(i).get("Phonenumberid")));
            data[i][0] = voiceCallRecordList.get(i);
        }
        return data;
    }  

    @Test(dataProvider = "voiceCallRecord")
    public void testGetVoiceCallRecords(VoiceCallRecord voiceCallRecord) throws DataCarrierException {
        List<VoiceCallRecord> voiceCallRecordListExcepted = Collections.singletonList(voiceCallRecord);
        when(teleDataDao.getCallRecords(eq(TENANT_ID), eq(voiceCallRecord.getPhonenumberid()), any(), any()))
                .thenReturn(voiceCallRecordListExcepted);
        List<VoiceCallRecord> voiceCallRecordListActual =
                service.getVoiceCallRecords(TENANT_ID, voiceCallRecord.getPhonenumberid().toString(), new Date(), new Date());
        assertEquals(voiceCallRecordListActual, voiceCallRecordListExcepted);
    }  

    public List<VoiceCallRecord> getVoiceCallRecords(String tenantId, String phoneid, Date startdate, Date
                enddate) throws DataCarrierException {
            List<VoiceCallRecord> callRecords;
            try {
                if (!mysqlAdaptService.needGetFromMysql(tenantId, phoneid)) {
                    UUID phoneNumberId = UUID.fromString(phoneid);
                    callRecords = teleDataDao.getCallRecords(tenantId, phoneNumberId, startdate, enddate);
                    if (callRecords == null || callRecords.size() == 0) {
                        throw new DataCarrierException(DataCarrierExceptionCode.EMPTY_QUERY_RESULT, "找不到对应的记录");
                    }
                } else {
                    TelLine line = telLineMapper.selectByPrimaryKey(Long.valueOf(phoneid));
                    List<TelSheet> telSheets = telSheetMapper.selectByLineId(getIndex(line.getPeopleID()), phoneid,
                            startdate, enddate);
                    callRecords = telSheets
                            .stream()
                            .peek(dataCarrierEncryptor::decryptAfterRetrieve)
                            .map(mysqlAdaptService::convertTelSheetToVoiceRecord)
                            .collect(Collectors.toList());

                    if (callRecords == null || callRecords.size() == 0) {
                        throw new DataCarrierException(DataCarrierExceptionCode.EMPTY_QUERY_RESULT, "找不到对应的记录");
                    }
                }

                callRecords.stream()
                        .peek(voiceCallRecord ->{
                            voiceCallRecord.setLocation(convertLocation(voiceCallRecord.getLocation()));//通话地点:将区号转为地名
                            voiceCallRecord.setDialtype(convertDetailType(voiceCallRecord.getDialtype()));//通话类型
                            voiceCallRecord.setTime(convertTime(voiceCallRecord.getTime()));//毫秒处理
                        }).collect(Collectors.toList());
            } catch (InvalidQueryException e) {
                LOGGER.error("getVoiceCallRecords meet invalid query exception: ", e);
                throw new DataCarrierException(DataCarrierExceptionCode.INVALID_QUERY_EXCEPTION, e);
            } catch (Exception e) {
                LOGGER.error("getVoiceCallRecords meet exception: ", e);
                throw new DataCarrierException(DataCarrierExceptionCode.GET_PEOPLE_BY_USER_ID_FAIL, e);
            }
            return callRecords;
        }
}

总结:
voiceCallRecordListExcepted和voiceCallRecordListActual 共享同一个内存空间,所以assertEquals(voiceCallRecordListActual, voiceCallRecordListExcepted);恒定是true,就是一段没有用的assert

代码中有很多convert,还有各类的分支,都没有覆盖
正确的写法

几个convert拆开,单独写单测,如:

public class PhoneQueryService{
    @Autowired
    private CpToCityReader cpToCityReader;
    public String convertLocation(String originLocation) {
        Map<String, String> cpToCityLists = cpToCityReader.getCpToCityMap();
        if (StringUtils.isNotBlank(originLocation)) {
            Pattern patternWithZero = Pattern.compile("0\\d{2,3}");
            Pattern patternWithoutZero = Pattern.compile("\\d{3}");
            Matcher matcher = patternWithZero.matcher(originLocation);
            if (matcher.find()) {
                originLocation =  matcher.group();
                return cpToCityLists.getOrDefault(originLocation, originLocation);
            } else {
                matcher = patternWithoutZero.matcher(originLocation);
                if (matcher.find()) {
                    originLocation = "0"+matcher.group();
                    return cpToCityLists.getOrDefault(originLocation, originLocation);
                }
            }
        }
        return originLocation;
    }
}


    public class PhoneQueryServiceTest {
        @Test
        public void testConvertLocation()throws DataCarrierException {
            ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
            map.put("0571","杭州");
            when(cpToCityReader.getCpToCityMap()).thenReturn(map);
            Assert.assertEquals(service.convertLocation("0571"),"杭州");
            Assert.assertEquals(service.convertLocation("571"),"杭州");
            Assert.assertEquals(service.convertLocation("[571]"),"杭州");
            Assert.assertEquals(service.convertLocation("[571]杭州"),"杭州");
            Assert.assertEquals(service.convertLocation("[0571]"),"杭州");
            Assert.assertEquals(service.convertLocation("杭州"),"杭州");
        }
    }

server方法覆盖
a) 数据驱动文件包含请求参数和预期结果
b) 预期结果不作变更直接和方法调用的结果做对比

public class XXXModel{
    List<VoiceCallRecord>  request;
    List<VoiceCallRecord>  expect;
    /** getter and setter */
}

public class PhoneQueryServiceTest {
    @DataProvider(name = "voiceCallRecord")
    public Iterator<Object[]> voiceCallRecord() throws IOException {
        List<Object[]> objectList = new ArrayList<>();
        List<XXXModel> xxxModelList = objectMapper.readValue(ClassLoader.getSystemResource("xxx.json"), new TypeReference<List<XXXModel>>() {
        });
        for (XXXModel xxxModel : xxxModelList){
            objectList.add(new Object[]{xxxModel});
        }
        return objectList.iterator();
}  

@Test(dataProvider = "voiceCallRecord")
    public void testGetVoiceCallRecords(XXXModel xxxModel) throws DataCarrierException {
        List<VoiceCallRecord> xxxxRequest = xxxModel.getRequest();
         List<VoiceCallRecord>  xxxxExpect = xxxModel.getExpect();
        when(teleDataDao.getCallRecords(eq(TENANT_ID), eq(voiceCallRecord.getPhonenumberid()), any(), any()))
                .thenReturn(xxxxRequest);
        List<VoiceCallRecord> voiceCallRecordListActual =
                service.getVoiceCallRecords(TENANT_ID, voiceCallRecord.getPhonenumberid().toString(), new Date(), new Date());
        assertEquals(voiceCallRecordListActual, xxxxExpect);
    }
}

2. 代码审查

所有人都要经过代码审查。并且很正规的:这种事情应该成为任何重要的软件开发工作中一个基本制度。并不单指产品程序——所有东西。它不需要很多的工作,但它的效果是巨大的。
从代码审查里能得到什么?
1.防止bug混入,他不是最重要的一点
2.代码审查的最大的功用是纯社会性的。如果你在编程,而且知道将会有同事检查你的代码,你编程态度就完全不一样了。你写出的代码将更加整洁,有更好的注释,更好的程序结构——因为你知道,那个你很在意的人将会查看你的程序。没有代码审查,你知道人们最终还是会看你的程序。但这种事情不是立即发生的事,它不会给你带来同等的紧迫感,它不会给你相同的个人评判的那种感受。
3.还有一个非常重要的好处。代码审查能传播知识。在很多的开发团队里,经常每一个人负责一个核心模块,每个人都只关注他自己的那个模块。除非是同事的模块影响了自己的程序,他们从不相互交流。这种情况的后果是,每个模块只有一个人熟悉里面的代码。如果这个人休假或——但愿不是——辞职了,其他人则束手无策。通过代码审查,至少会有两个人熟悉这些程序——作者,以及审查者。审查者并不能像程序的作者一样对程序十分了解——但他会熟悉程序的设计和架构,这是极其重要的。
4.最重要的一个原则:代码审查用意是在代码提交前找到其中的问题——你要发现是它的正确。在代码审查中最常犯的错误——几乎每个新手都会犯的错误——是,审查者根据自己的编程习惯来评判别人的代码。
5.第二个误区就是人们感觉一定要说点什么(才算是做了代码审查)。代码审查的第二个易犯的毛病是,人们觉得有压力,感觉非要说点什么才好。你知道作者用了大量的时间和精力来实现这些程序——不该说点什么吗?不,你不需要。只说一句“哇,不错呀”,任何时候都不会不合适。如果你总是力图找出一点什么东西来批评,你这样做的结果只会损害自己的威望。当你不厌其烦的找出一些东西来,只是为了说些什么,被审查人就会知道,你说这些话只是为了填补寂静。你的评论将不再被人重视
6.第三个误区就是速度。。你不能匆匆忙忙的进行一次代码审查——但你也要能迅速的完成。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

代码质量(单元测试+代码审查) 的相关文章

  • ardupilot开发 --- 避障篇

    避障的类型 空中防碰撞ADSB 主要是防止与其他飞行器的碰撞 避障 防止与天花板地板障碍物的碰撞 实现避障必要的传感器 ADSB receivers Rangefinders or Proximity Sensors or Realsens

随机推荐

  • 给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中最后一个单词的长度。

    提示 题目答案均由博主自主编写 想法不一 答案也不一 本答案仅提供参考 如有疑问 可在评论区提问 有时间会解答 题目描述 给你一个字符串 s 由若干单词组成 单词前后用一些空格字符隔开 返回字符串中最后一个单词的长度 单词是指仅由字母组成
  • 用python语言判断素数(质数)

    今天查了很多关于判断质数的代码 自己也尝试写了一下 质数是指在大于1的自然数中 除了1和它本身以外不再有其他因数的自然数 所有我们能很容易的想到使用for循环来实现输入数m和 2 m 1 的相除 代码实现 m eval input 请输入一
  • web服务器接口文档,接口文档

    有字库接口文档 由于中文字体文件过大 有字库采用 按需截取 根据页面内容把字体中不需要的字型删除掉 的方案 将中文字体压缩成和英文字体一样小巧玲珑 按需截取 与 整套嵌入 方案相比 1 按需截取 生成的中文字体只有十几K至一百多K 而 整套
  • 关于测试的静态方法学习笔记

    在学习软件测试时的一些笔记 可以加深对测试的理解 静态测试技术概述 1 静态测试是不执行被分析的程序 而是通过对模块源代码进行研读 找出其中的错误或可疑之处 收集一些度量数据 2 静态测试包括对软件产品的需求和设计规格说明书的评审 对程序代
  • docker安装wordpress

    参考文章 步骤 1 安装docker 2 下载wordpress镜像 3 下载mysql镜像 4 启动mysql容器 5 启动wordpress容器 遇到的问题 1 进入wordpress报数据库错误 猜测是连不上数据库 在宿主机尝试连接M
  • Qt on Android 之设置应用名为中文

    今早群里有个盆友问如何将 Qt 开发的 Android 应用的名字设置为中文 试验了一下 有两个办法 直接修改 AndroidManifest xml 文件 首先你在创建 Qt on Android 工程时需要创建一个 AndroidMan
  • Gmail,Qmail,163等邮件服务器SMTP、IMAP、POP3、地址及SSL/非SSL协议端口号

    最近项目需要给后台发送邮件 将项目中部分信息与后台同步 于是就有了这篇博客 以下的内容是参考网上的例子加上自己实践总结了一下 邮箱默认配置 服务器名称 服务器地址 SSL协议端口号 非SSL协议端口号 IMAP imap xx com 99
  • WebService客户端几种实现方式

    文章目录 一 发布一个webservice服务 jdk原生 1 编写服务接口 2 服务实现类 3 发布服务 4 浏览器查看是否发布成功 二 几种客户端调用方式 1 jdk原生调用 需要获取服务接口文件 2 用import命令生成客户端代码
  • idea远程断点调试

    在idea里面配置远程断点调试 192 168 198 130 是远程服务端口 5005是远程服务连接端口 在linux启动在线服务 在启动服务里面加入参数 Xdebug agentlib jdwp transport dt socket
  • ArtPi 认识RTT Studio建立LED工程

    1 认识RTT Studio建立LED工程 软件IDE RT Thread Studio 版本 2 1 1 硬件平台 ART Pi CPU STM32H750XB 开发板基本外设功能实现 串口 uart4 PA0 PI9 Red LED P
  • vmware搭建centos虚拟机并使用静态ip,局域网内可互通

    一 虚拟机镜像地址 我这里有镜像 二 目的 使用vmware搭建centos虚拟机集群 进行基础服务搭建 对系统业务提供服务支撑 三 效果 centos虚拟机ip不会自动改变 使用设置的静态ip 可以整个局域网互相访问 四 实现 1 宿主机
  • 密室逃生游戏【C语言】

    字符串 逻辑分析 小强在参加 密室逃生 游戏 当前关卡要求找到符合给定密码K 升序的不重复小写字母组成 的箱子 并给出箱子编号 箱子编号为1 N 每个箱子中都有一个字符串s 字符串由大写字母 小写字母 数字 标点符号 空格组成 需要在这些字
  • [从零开始学DeepFaceLab-13]: 使用-命令行八大操作步骤-第6步:模型的选择与训练 - 常见基本问题

    目录 前言 1 如何关闭训练 2 如何保存进度 大多情况下没有必要
  • NLP GPT算法笔记

    从这个意义上讲 我们可以说GPT 2本质上是键盘应用程序的下一个单词预测功能 但是它比您的手机具有更大 更复杂的功能 GPT 2在称为WebText的庞大40GB数据集上进行了训练 作为研究工作的一部分 OpenAI研究人员从互联网上进行了
  • 分布式Netty集群方案 加代码 SpringBoot 版

    目录 单机netty是怎么通信的 多节点集群netty是怎么通信的呢 netty集群是怎么搭建的呢 连接上的 client 的 channelId 怎么存入 redis 中 在集群模式中 客户端1向客户端2发送信息 演示效果 完整的讲解 n
  • unity_控制物体移动代码

    目录 2D游戏控制 简单的上下左右移动 第一种 使用Rigidbody2D 第二种 上下左右移动加上旋转 2D空战飞机的移动 汽车 坦克等移动 坦克的控制 2D游戏控制 简单的上下左右移动 第一种 使用Rigidbody2D using S
  • css3绘制扫描图片效果

    html
  • KMP算法(思想真的不复杂)

    在了解KMP之前 我们需要了解两个概念 字符串的前缀 和字符串的后缀 字符串的前缀 我举个例子你们就懂了 一个字符串abcde 它包含的前缀有 a ab abc abcd 字符串的后缀 bcde cde de e 知道这两个概念后 我们就可
  • 欧式距离计算公式

    欧式距离也称欧几里得距离 是最常见的距离度量 衡量的是多维空间中两个点之间的绝对距离 也可以理解为 m维空间中两个点之间的真实距离 或者向量的自然长度 即该点到原点的距离 在二维和三维空间中的欧氏距离就是两点之间的实际距离 下面是具体的计算
  • 代码质量(单元测试+代码审查)

    代码质量 1 单元测试 2 代码审查 1 单元测试 单元测试的目的 尽早在尽量小的范围内暴露错误 错误率恒定定律 一定量的代码 必然会产生一定量的BUG a 刚写完一个方法就发现BUG 修改只要几分钟 方法提供给其他人使用后 再发现BUG