这篇随笔是一篇记录性的随笔,记录了从http://www.sikiedu.com/my/course/304,这门课程中学到的内容,附带了一些自己的思考。

一.单例模式的应用

首先假想一种情况,现在需要有一个按钮和一个Text,当按下按钮时,Text上显示“你好”两个字。

一个最常见的方法是在按钮下挂载一个脚本BtnClick,它持有一个Text组件,它由外部的Text拖入来赋值。

在初始化时BtnClick会获取当前游戏物体下的Button组件并为其添加监听,当按下按钮时会修改Text组件中的文本内容。

具体的效果图和代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; public class BtnClick : MonoBehaviour { // Use this for initialization public Text myText; void Awake ()
{
GetComponent<Button>().onClick.AddListener(()=>
{
myText.text = "你好";
}); }
}

BtnClick脚本

BtnClick中为Button组件添加的监听的方法是用lambda表达式写的,不懂的自行查阅资料。

这种方式有两个问题,一是耦合度过高,假如Text组件不小心被删除,点击按钮会因为找不到Text组件而报错。 二是只能作一对一的操作,无法实现复杂的交互,举个例子,假如现在有3个如上图所示的按钮,同时只有1个Text,现在要实现一个“累加”的功能,任意一个按钮被按下时,计数会加1,当累计计数到3时,让按钮显示“你好”两个字。

假如修改代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; public class BtnClick : MonoBehaviour { // Use this for initialization public Text myText;
private int number; void Awake ()
{
GetComponent<Button>().onClick.AddListener(()=>
{
number++;
if (number >= )
{
myText.text = "你好";
}
}); }
}

BtnClick脚本

此时对一个按钮按3下可以让按钮显示“你好”两个字,那如果我们复制3个相同的按钮,将Text组件拖放到3个按钮的BtnClick脚本中,是否可以满足要求?很显然,不行,因为定义的number属于类本身,3个脚本各自有属于自己的number,所以3个按钮的点击会分别叠加,没有累加效果。

那怎么办?难道要定义个全局变量来累加吗?那太蠢了,而且会很混乱。

我们先来解决问题二:

一个最常见的解决方法是为要操作的组件添加交互脚本,并在脚本中使用单例模式:

我们可以在Text组件下挂载一个脚本ShowText,脚本有一个计数器number,并提供一个Show方法,每次调用Show方法会增加计数,当计数满足条件时会获取Text组件并修改上面的内容。同时在按钮下挂载另一个脚本BtnClick,它在初始化时会获取Button组件并为其添加监听,当按下按钮时会调用ShowText脚本中的Show方法,这样可以起到累加的作用。

具体的效果图和代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; public class ShowText : MonoBehaviour
{
public static ShowText Instance;
private int number;
private void Awake()
{
Instance = this;
}
public void Show(string str)
{
number++;
if (number >= )
{
GetComponent<Text>().text = str;
}
}
}

ShowText脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; public class BtnClick : MonoBehaviour { // Use this for initialization void Awake ()
{
GetComponent<Button>().onClick.AddListener(()=>
{
ShowText.Instance.Show("你好");
}); }
}

BtnClick脚本

你可能会有疑惑,为什么要用单例模式?

要知道,我们这里之所以能实现累加,是因为3个Button的BtnClick都调用了同一个ShowText脚本中的Show方法,从而使计数能被累加。单例模式是为了保证3个BtnClick获取到的都是同一个ShowText实例对象。假如我们不用单例模式,BtnClick中点击事件的回调方法要想调用ShowText中的Show方法,就只能实例化一个对象,先不说Unity下继承自MonoBehaviour的类无法通过new来实例化,就算它可以,我们就只能这样做:

ShowText myShowText = new ShowText();

myShowText.Show();

显然,这种情况下3个按钮下BtnClick获取到的是3个不同的新创建的ShowText脚本,里面的number属性自然也无法一起累加,只能各自累加了。

虽然使用单例模式为我们解决了问题二,但仍没有解决问题一,若Text意外被删除,BtnClick会因为获取不到ShowText的单例对象而报错。

此外,即使不考虑问题一,我们考虑一种情况:假若现在不是3个按钮共同控制1个Text,而是1个按钮控制3个Text,当按下按钮时,需要让3个Text同时显示出"你好"。若用单例模式处理,在BtnClick的添加的监听方法中,需要获取每一个Text的单例对象,假若这一个按钮不只有这些Text要控制,还要控制许多其他的UI控件,那会使代码变得十分臃肿。

如何解决这两个问题?

其实这两个问题主要的症结在于当按钮被按下时,我们在按钮下挂载的脚本中直接去访问了Text组件,大大增加耦合度的同时使代码变得十分臃肿。那为了使按钮按下时不去直接访问Text组件,我们必须设计一层中间的过渡脚本,按钮按下时会到过渡脚本中访问Text组件中的方法,而且在过渡脚本中也不能直接访问Text组件下的脚本,否则一旦Text被删,一样会导致报错,但这是不可能的,过渡脚本若不访问Text组件下的脚本,怎么调用脚本中的方法呢?

我们可以退一步思考,若在Text被删的时候自动让过渡脚本知道,Text下挂载的脚本中的方法已经不存在,从而不再访问,而在Text被创建时也能让过渡脚本知道,此方法已经可以被调用了,是不是可以解决这两个问题呢?显然是的,这么做的话耦合度被大大降低,BtnClick只负责访问过渡脚本,过渡脚本中自己可以识别Text下挂载的脚本的方法是否存在,不存在就不调用,即使Text被意外删除,调用BtnClick也不会报错。

用什么机制来实现呢?用委托与监听来解决,使用委托是为了解决BtnClick关联到许多Text时需要获取很多单例带来的代码臃肿问题,委托可以看作一种函数指针,C#中可以把某些方法作为参数进行传递,参数的类型就是委托,而委托有一种很关键的特性,就是可以相加,一个委托+另一个委托,相当于同时关联了两个方法,调用这个委托时相当于同时对这两个方法进行调用。(当然,委托必须是同类型的才能叠加,比如方法的参数个数,类型要相等),我们可以利用委托的这种特性完成一个BtnClick对许多ShowText脚本下方法的同时调用。

委托的简要介绍可以参考:http://www.runoob.com/csharp/csharp-delegate.html

二.利用委托与监听解耦合

现在来描述具体实现思路:

过渡脚本中维护一个字典,字典中存放事件码(作为键)和对应的委托(作为值),事件码是一个枚举类型,由一个文件单独定义,每个事件码对应一种监听事件,比如按钮的点击,当有新的交互事件要定义时,可以手动添加新的事件码定义。字典中的值是委托类型,但委托对应的方法可以有参数,可以没参数,参数不同的委托无法相加,因此需要一个文件专门声明不同类型的委托,在过渡脚本中需要暴露3个方法,1个用来添加委托,这时就要作委托类型匹配的判断。另外一个用来移除委托,同样要类型验证,还要有一个广播方法,用来供按钮点击后调用,按钮点击时传递事件码,广播方法在字典中找到相应的委托并调用。

那什么时候进行委托的添加和移除呢?肯定是在Text初始化和销毁时,继承自MonoBehavior的类由Unity负责初始化和销毁,Unity初始化它时会调用Awake方法,销毁时会调用Destroy方法,我们在Awake时把供外部调用的方法添加到过渡脚本中的字典中,Destroy时从字典移除此委托,这就相当于让Unity来帮我们维护它们,这样,万一发生意外情况,Text被销毁了,Unity一定会调用Destroy,相应的委托就会从字典中消失,这样即使广播方法在字典中找不到对应的委托,也不至于报错,因为它并没有直接去获取并使用Text上的组件

这样,我们就需要3个文件,1个用来定义事件码,1个用来定义不同参数的委托,1个用来维护管理事件码委托的字典,并提供外部调用。

先来最简单的,事件码的文件的代码,文件名:EventType.cs,代码如下:

public enum EventType
{
ShowText
}

EventType

当前只添加了一个事件码,需要时可手动添加

随后是定义不同参数委托的文件,文件名:CallBack.cs,代码如下:

public delegate void CallBack();
public delegate void CallBack<T>(T arg);
public delegate void CallBack<T, X>(T arg1, X arg2);
public delegate void CallBack<T, X, Y>(T arg1, X arg2, Y arg3);
public delegate void CallBack<T, X, Y, Z>(T arg1, X arg2, Y arg3, Z arg4);
public delegate void CallBack<T, X, Y, Z, W>(T arg1, X arg2, Y arg3, Z arg4, W arg5);

CallBack

这里重载了6个同名的委托,使用泛型使委托可以适应更多的情况,这些委托覆盖了参数个数从0到5的所有情况,相应地,在添加委托到字典时,就要设计多个重载的AddListener和RemoveListener函数,以便在添加和删除委托时使用相应的方法。

下面是过渡脚本,文件名:EventCenter.cs,代码如下:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine; public class EventCenter
{
private static Dictionary<EventType, Delegate> m_EventTable = new Dictionary<EventType, Delegate>(); private static void OnListenerAdding(EventType eventType, Delegate callBack)
{
if (!m_EventTable.ContainsKey(eventType))
{
m_EventTable.Add(eventType, null);
}
Delegate d = m_EventTable[eventType];
if (d != null && d.GetType() != callBack.GetType())
{
throw new Exception(string.Format("尝试为事件{0}添加不同类型的委托,当前事件所对应的委托是{1},要添加的委托类型为{2}", eventType, d.GetType(), callBack.GetType()));
}
}
private static void OnListenerRemoving(EventType eventType, Delegate callBack)
{
if (m_EventTable.ContainsKey(eventType))
{
Delegate d = m_EventTable[eventType];
if (d == null)
{
throw new Exception(string.Format("移除监听错误:事件{0}没有对应的委托", eventType));
}
else if (d.GetType() != callBack.GetType())
{
throw new Exception(string.Format("移除监听错误:尝试为事件{0}移除不同类型的委托,当前委托类型为{1},要移除的委托类型为{2}", eventType, d.GetType(), callBack.GetType()));
}
}
else
{
throw new Exception(string.Format("移除监听错误:没有事件码{0}", eventType));
}
}
private static void OnListenerRemoved(EventType eventType)
{
if (m_EventTable[eventType] == null)
{
m_EventTable.Remove(eventType);
}
}
//no parameters
public static void AddListener(EventType eventType, CallBack callBack)
{
OnListenerAdding(eventType, callBack);
m_EventTable[eventType] = (CallBack)m_EventTable[eventType] + callBack;
}
//Single parameters
public static void AddListener<T>(EventType eventType, CallBack<T> callBack)
{
OnListenerAdding(eventType, callBack);
m_EventTable[eventType] = (CallBack<T>)m_EventTable[eventType] + callBack;
}
//two parameters
public static void AddListener<T, X>(EventType eventType, CallBack<T, X> callBack)
{
OnListenerAdding(eventType, callBack);
m_EventTable[eventType] = (CallBack<T, X>)m_EventTable[eventType] + callBack;
}
//three parameters
public static void AddListener<T, X, Y>(EventType eventType, CallBack<T, X, Y> callBack)
{
OnListenerAdding(eventType, callBack);
m_EventTable[eventType] = (CallBack<T, X, Y>)m_EventTable[eventType] + callBack;
}
//four parameters
public static void AddListener<T, X, Y, Z>(EventType eventType, CallBack<T, X, Y, Z> callBack)
{
OnListenerAdding(eventType, callBack);
m_EventTable[eventType] = (CallBack<T, X, Y, Z>)m_EventTable[eventType] + callBack;
}
//five parameters
public static void AddListener<T, X, Y, Z, W>(EventType eventType, CallBack<T, X, Y, Z, W> callBack)
{
OnListenerAdding(eventType, callBack);
m_EventTable[eventType] = (CallBack<T, X, Y, Z, W>)m_EventTable[eventType] + callBack;
} //no parameters
public static void RemoveListener(EventType eventType, CallBack callBack)
{
OnListenerRemoving(eventType, callBack);
m_EventTable[eventType] = (CallBack)m_EventTable[eventType] - callBack;
OnListenerRemoved(eventType);
}
//single parameters
public static void RemoveListener<T>(EventType eventType, CallBack<T> callBack)
{
OnListenerRemoving(eventType, callBack);
m_EventTable[eventType] = (CallBack<T>)m_EventTable[eventType] - callBack;
OnListenerRemoved(eventType);
}
//two parameters
public static void RemoveListener<T, X>(EventType eventType, CallBack<T, X> callBack)
{
OnListenerRemoving(eventType, callBack);
m_EventTable[eventType] = (CallBack<T, X>)m_EventTable[eventType] - callBack;
OnListenerRemoved(eventType);
}
//three parameters
public static void RemoveListener<T, X, Y>(EventType eventType, CallBack<T, X, Y> callBack)
{
OnListenerRemoving(eventType, callBack);
m_EventTable[eventType] = (CallBack<T, X, Y>)m_EventTable[eventType] - callBack;
OnListenerRemoved(eventType);
}
//four parameters
public static void RemoveListener<T, X, Y, Z>(EventType eventType, CallBack<T, X, Y, Z> callBack)
{
OnListenerRemoving(eventType, callBack);
m_EventTable[eventType] = (CallBack<T, X, Y, Z>)m_EventTable[eventType] - callBack;
OnListenerRemoved(eventType);
}
//five parameters
public static void RemoveListener<T, X, Y, Z, W>(EventType eventType, CallBack<T, X, Y, Z, W> callBack)
{
OnListenerRemoving(eventType, callBack);
m_EventTable[eventType] = (CallBack<T, X, Y, Z, W>)m_EventTable[eventType] - callBack;
OnListenerRemoved(eventType);
} //no parameters
public static void Broadcast(EventType eventType)
{
Delegate d;
if (m_EventTable.TryGetValue(eventType, out d))
{
CallBack callBack = d as CallBack;
if (callBack != null)
{
callBack();
}
else
{
throw new Exception(string.Format("广播事件错误:事件{0}对应委托具有不同的类型", eventType));
}
}
}
//single parameters
public static void Broadcast<T>(EventType eventType, T arg)
{
Delegate d;
if (m_EventTable.TryGetValue(eventType, out d))
{
CallBack<T> callBack = d as CallBack<T>;
if (callBack != null)
{
callBack(arg);
}
else
{
throw new Exception(string.Format("广播事件错误:事件{0}对应委托具有不同的类型", eventType));
}
}
}
//two parameters
public static void Broadcast<T, X>(EventType eventType, T arg1, X arg2)
{
Delegate d;
if (m_EventTable.TryGetValue(eventType, out d))
{
CallBack<T, X> callBack = d as CallBack<T, X>;
if (callBack != null)
{
callBack(arg1, arg2);
}
else
{
throw new Exception(string.Format("广播事件错误:事件{0}对应委托具有不同的类型", eventType));
}
}
}
//three parameters
public static void Broadcast<T, X, Y>(EventType eventType, T arg1, X arg2, Y arg3)
{
Delegate d;
if (m_EventTable.TryGetValue(eventType, out d))
{
CallBack<T, X, Y> callBack = d as CallBack<T, X, Y>;
if (callBack != null)
{
callBack(arg1, arg2, arg3);
}
else
{
throw new Exception(string.Format("广播事件错误:事件{0}对应委托具有不同的类型", eventType));
}
}
}
//four parameters
public static void Broadcast<T, X, Y, Z>(EventType eventType, T arg1, X arg2, Y arg3, Z arg4)
{
Delegate d;
if (m_EventTable.TryGetValue(eventType, out d))
{
CallBack<T, X, Y, Z> callBack = d as CallBack<T, X, Y, Z>;
if (callBack != null)
{
callBack(arg1, arg2, arg3, arg4);
}
else
{
throw new Exception(string.Format("广播事件错误:事件{0}对应委托具有不同的类型", eventType));
}
}
}
//five parameters
public static void Broadcast<T, X, Y, Z, W>(EventType eventType, T arg1, X arg2, Y arg3, Z arg4, W arg5)
{
Delegate d;
if (m_EventTable.TryGetValue(eventType, out d))
{
CallBack<T, X, Y, Z, W> callBack = d as CallBack<T, X, Y, Z, W>;
if (callBack != null)
{
callBack(arg1, arg2, arg3, arg4, arg5);
}
else
{
throw new Exception(string.Format("广播事件错误:事件{0}对应委托具有不同的类型", eventType));
}
}
}
}

EventCenter

这里有很多要注意的点:

1.先看AddListener系列的方法,6个方法分别对应传入6种不同参数个数的委托,之所以每个里面要分两步完成,是为了进行代码精简,OnListenerAdding将6种AddListener的相同部分抽取了出来,注意为了实现抽取使用了多态,用Delegate容纳不同CallBack参数。

简单讲述下AddListener的逻辑:

1.先判断字典中有无对应事件码,没有就添加新的,新的事件码对应的的委托方法设为null

2.取出事件码对应的委托,如果这个委托不是null,且它的类型和传入的CallBack类型不同,则抛出异常。

3.经历了以上两步,显然此时事件码对应的委托要不就是null,要不就是和传入的CallBack类型相同,此时根据AddListener的不同将事件码对应的委托转换  成相同的类型并进行相加,重新赋回到字典中的对应项。

这里有一点很关键:仔细想,为什么我们要把AddListener分成那么多类型,传入不同的CallBack,能不能利用多态,第二个参数能不能直接传入Delegate?

答案是不行,因为上面的第2步会将字典中的相应Delegate与传入的CallBack进行类型比较,在同类型的情况下,第3步会把事件码对应的委托转换成传入的CallBack的类型并加回到字典中,假如传入的就是Delegate,那在第3步根本不存在类型转换,那存入字典的就一定是Delegate类型,那就根本没法在下一个第2步找出类型不同的情况。

同理,叙述一下RemoveListener的逻辑:

1.先判断字典中有无对应事件码,没有就直接抛出异常。

2.有的话取出事件码对应的委托,如果这个委托为null,抛出异常。如果不为null且它的类型和传入的CallBack类型不同,也抛出异常。

3.经历了以上两步,显然此时事件码对应的委托一定和传入的CallBack类型相同,此时根据AddListener的不同将事件码对应的委托转换成相同的类型并进行相减。

4.减完后要判断事件码对应的委托是不是null,是的话移除事件码及对应的委托。

BroadCast也一样要分多种参数类型,因为有可能要传入不同数量的参数,BroadCast的逻辑:

1.用TryGet获取对应事件码的委托,若获取失败不做任何操作(这一条保证了不会报错)

2.若获取成功,将此委托强转为对应的CallBack,这一条很重要,这就是为什么BroadCast也要分多种参数类型原因,不分的话,不知道要把委托转换成哪种具体的CallBack,而以Delegate为类型,自然无法调用相应的参数。

3.判断强转的CallBack是不是null,是代表强转失败,这是因为事件码对应的委托的参数并不是此类BroadCast能处理的,此时要抛出异常。

4.若CallBack不是null,代表强转成功,直接调用方法即可。

接下来给出测试用的ShowText脚本(挂载在Text组件上)和BtnClick脚本(挂载在按钮上)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; public class ShowText : MonoBehaviour
{
private void Awake()
{
gameObject.SetActive(false);
EventCenter.AddListener<string, string, float, int, int>(EventType.ShowText, Show);
}
private void OnDestroy()
{
EventCenter.RemoveListener<string, string, float, int, int>(EventType.ShowText, Show);
}
private void Show(string str,string str1,float a, int b, int c)
{
gameObject.SetActive(true);
GetComponent<Text>().text = str + str1 + a + b + c;
}
}

ShowText脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; public class BtnClick : MonoBehaviour { // Use this for initialization private void Awake ()
{
GetComponent<Button>().onClick.AddListener(()=>
{
EventCenter.Broadcast(EventType.ShowText,"你好","呀",1.0f,,);
}); }
}

BtnClick脚本

这个没什么好说的,ShowText在Awake时把Show方法(这里用5个参数的作演示)添加到字典,在Destroy中作移除。

BtnClick在被点击时调用BroadCast并传入相应的参数。

有一点要注意:AddListener的调用要明确指明使用的泛型,<string,string,float,int,int>,若直接

EventCenter.AddListener(EventType.ShowText, Show);

会报错,这是因为不同版本的AddListener参数个数是相同的,因此调用时若不指定调用的AddListener版本,程序不知道调用哪个。RemoveListener也是同理。但是,对BroadCast的调用则不必指定版本,因为不同版本的BroadCast参数个数不同,根据传入的参数个数,程序能自动识别调用哪个版本的BroadCast。

这种利用委托和监听解耦合的思路也是有缺点的,就是调用时顺序必须匹配,若ShowText以<string,string,float,int,int>的方式添加了委托,则调用时也必须以同样的顺序传入参数,而我们事先无法得知具体的顺序是怎样的,要在BtnClick实现调用,就只能通过跳转代码,跳转到ShowText的源代码中去了解调用顺序。

另外,我自己的看法是,这种方式可能会破坏封装性,因为ShowText中的Show方法,在ShowText中是被定义为private的,而这个方法又在ShowText被初始化的时候作为委托被添加到了EventCenter中的字典里,按下按钮时根据事件码从字典中找到对应的委托并调用,相当于在ShowText类外部可以随意对类内的私有private方法进行随意调用,安全性似乎有点问题。但这不是由这种思路带来的,而是委托机制本身带来的缺陷,抛开这门课不讲,一个方法可以通过委托传递可以让人接受,但一个类中的私有方法可以通过委托被传递,从而供外部函数随意调用,就有点奇怪了,在我看来,至少也应该设计成只有类中的公有方法才能通过委托传递才对啊,C#里的这种委托机制真的不会带来安全性的隐患吗?这只是我的一点小疑惑,可能是目前对委托的理解不够深,等以后明白了再回来填坑吧。

Unity中利用委托与监听解耦合的思路的更多相关文章

  1. onscroll事件没有响应的原因以及vue.js中添加onscroll事件监听的方法

    1 onscroll事件失效 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...

  2. 关于JAVA中事件分发和监听机制实现的代码实例-绝对原创实用

    http://blog.csdn.net/5iasp/article/details/37054171 文章标题:关于JAVA中事件分发和监听机制实现的代码实例 文章地址: http://blog.c ...

  3. Android 开发中的View事件监听机制

    在开发过程中,我们常常根据实际的需要绘制自己的应用组件,那么定制自己的监听事件,及相应的处理方法是必要的.我们都知道Android中,事件的监听是基于回调机制的,比如常用的OnClick事件,你了解它 ...

  4. 【Unity编程】Unity中关于四元数的API详解

    本文为博主原创文章,欢迎转载,请保留出处:http://blog.csdn.net/andrewfan Unity中关于四元数的API详解 Quaternion类 Quaternion(四元数)用于计 ...

  5. 利用select/poll监听多个设备详解

    如果一个应用程序去处理多个设备,例如应用程序读取网路数据,按键,串口,一般能想到的有三种方法: 方法1:串行+阻塞的方式读取:while(1) { read(标准输入);read(网络);}缺点:每当 ...

  6. 详解 RAC 中各种IP和监听的意义

    一.SCAN 概念 SCAN(Single Client Access Name)是 Oracle从11g R2开始推出的,客户端可以通过 SCAN 特性负载均衡地连接到 RAC数据库 SCAN 最明 ...

  7. jQuery中四种事件监听的区别

    原文链接:点我 我们知道jquery提供了四种事件监听方式,分别是bind.live.delegate.on,下面就分别对这四种事件监听方式分析. 已知有4个列表元素: 列表元素1 列表元素2 列表元 ...

  8. React和Vue中,是如何监听变量变化的

    React 中事件监听 本地调试React代码的方法 先将React代码下载到本地,进入项目文件夹后yarn build 利用create-react-app创建一个自己的项目 把react源码和自己 ...

  9. 利用支付宝Cookie监听交易订单实现个人支付宝收款实时回调通知

    在网上.社区里搜了一下好像没找到什么文章详细分享这种方式的,这些天我花了些时间研究整理了一下,发现这种方式能实时获取到支付宝里的二维码收款记录,从而很好地实现个人支付宝免签约收款实时回调,于是在这里分 ...

随机推荐

  1. hadoop理解

    Hadoop的主核心有2部分: 1,HDFS 2, MapReduce 首先: HDFS HDFS(Hadoop Distributed File System,Hadoop分布式文件系统),它是一个 ...

  2. Delphi IOS开发环境安装

    RAD Delphi XE/10 Seattle 安装IOS.OSX环境安装,IOS模拟器,MAC X 真机可以调试 http://community.embarcadero.com/blogs/en ...

  3. 转:javascript判断IE浏览器

    http://blog.csdn.net/ranbolwb/article/details/18555847 function isIE() { //ie? if (!!window.ActiveXO ...

  4. elasticsearch(0.90.10)安装配置+超多插件!!

    一)安装elasticsearch 1)下载elasticsearch-0.90.10,解压,运行\bin\elasticsearch.bat (windwos) 2)进入http://localho ...

  5. C# Log4.Net日志组件的应用系列(二)

    引言 Log4Net应该可以说是.NET中最流行的开源日志组件了.在各种项目框架中可以说是必不可少的组成部分.个人认为Log4Net有下面几个优点: 1. 使用灵活,它可以将日志分不同的等级,以不同的 ...

  6. python3.7.0安装

    如何安装Python的操作步骤: 1.第一步先去python的官方网站下载python的安装包 地址:https://www.python.org/downloads/ 根据自己的系统选择对应的安装包 ...

  7. read_csv 函数

    转载自 https://www.cnblogs.com/datablog/p/6127000.html pandas.read_csv参数整理 读取CSV(逗号分割)文件到DataFrame也支持文件 ...

  8. POJ - 1251A - Jungle Roads 利用最小生成树

    The Head Elder of the tropical island of Lagrishan has a problem. A burst of foreign aid money was s ...

  9. 一个标准的AJAX请求

    这是一个标准的ajax请求: $.ajax({ type:"post", url:basePath+"/resourcePush/operationLog", ...

  10. 20 行代码极速为 App 加上聊天功能

    现在很多 App 都需要集成 IM 功能,今天就为大家分享一下集成 IM 基本功能的步骤.本文内容以 JMessage 为例.极光 IM ( JMessage ) = 极光推送 ( JPush ) + ...